「テストを先に書く」と言われても、何から始めればいいかわからない
「テスト駆動開発(TDD)が良いのは知っている。でも、具体的にどう始めればいいのかわからない」——開発現場でこう感じている方は多いのではないでしょうか。
TDDの概念は理解できても、いざ実践しようとすると「最初のテストは何を書けばいい?」「どこまで細かくテストすべき?」と手が止まってしまいがちです。
この記事では、テスト駆動開発のやり方を5つのステップに分解し、Pythonのコード例を交えながら実践的に解説します。読み終わるころには、TDDの基本サイクルを自分の開発に取り入れられるようになっているはずです。
テスト駆動開発(TDD)とは?30秒でわかる基本
テスト駆動開発(Test-Driven Development)とは、「実装コードより先にテストコードを書く」 開発手法です。
従来の開発では「コードを書く → テストで確認する」という流れが一般的でした。TDDはこの順番を逆転させ、「テストを書く → テストを通すコードを書く → リファクタリングする」というサイクルを繰り返します。
この基本サイクルは、テスト失敗時の赤い表示・成功時の緑の表示にちなんで 「Red - Green - Refactor」 と呼ばれています。
| フェーズ | やること | 状態 |
|---|---|---|
| Red | 失敗するテストを書く | テスト失敗(赤) |
| Green | テストを通す最小限のコードを書く | テスト成功(緑) |
| Refactor | コードをきれいに整理する | テスト成功を維持 |
このサイクルを小さく・速く回すのがTDDの核心です。では、具体的なやり方を見ていきましょう。
テスト駆動開発のやり方——5ステップ実践ガイド
ここからは、Pythonで「税込み価格を計算する関数」を題材に、TDDの5ステップを体験していきます。テストフレームワークには pytest を使用します。
ステップ1:テストケースを書く(Red)
まず、実装したい機能の「期待する動作」をテストコードで表現します。この時点では実装コードは存在しないので、テストは必ず失敗します。
ここでのポイントは、いきなり全部のケースを網羅しようとしないこと。最もシンプルなケースから始めます。
# test_tax.py
from tax_calculator import calculate_tax_included_price
def test_basic_price():
"""基本的な税込み価格の計算"""
assert calculate_tax_included_price(1000, 0.10) == 1100
def test_zero_price():
"""0円の場合"""
assert calculate_tax_included_price(0, 0.10) == 0
def test_rounding():
"""端数は切り捨て"""
assert calculate_tax_included_price(198, 0.10) == 217 # 198 * 1.10 = 217.8 → 217
テストを実行すると、tax_calculator モジュールが存在しないため当然エラーになります。
$ pytest test_tax.py
# ModuleNotFoundError: No module named 'tax_calculator'
この「失敗を確認する」ことが大切です。テストが正しく機能している証拠だからです。
ステップ2:テストが失敗することを確認する(Red)
ステップ1で書いたテストを実行し、意図通りに失敗していることを確認します。
「当然失敗するのでは?」と思うかもしれませんが、このステップには2つの意味があります。
- テストコード自体にバグがないか確認する:テストが常に成功してしまう書き方をしていたら、そのテストは何の検証にもなりません
- エラーメッセージから要件を再確認する:エラー内容を見て、テストが正しい観点をチェックしているか振り返ります
もしテストが成功してしまったら、テストの書き方に問題があります。要件の見直しから始めましょう。
ステップ3:テストを通す最小限のコードを書く(Green)
次に、テストを成功させるための最小限の実装コードを書きます。「きれいなコード」は後回しでOK。まずはテストを通すことだけに集中します。
# tax_calculator.py
import math
def calculate_tax_included_price(price, tax_rate):
"""税込み価格を計算する(端数切り捨て)"""
return math.floor(price * (1 + tax_rate))
テストを実行します。
$ pytest test_tax.py
# 3 passed
すべてのテストが通りました。まずはここで一安心です。
ステップ4:テストケースを追加する(Red → Green の繰り返し)
基本的なケースが通ったら、エッジケースや異常系のテストを追加していきます。
# test_tax.py に追加
def test_negative_price_raises_error():
"""負の金額はエラー"""
import pytest
with pytest.raises(ValueError):
calculate_tax_included_price(-100, 0.10)
def test_negative_tax_rate_raises_error():
"""負の税率はエラー"""
import pytest
with pytest.raises(ValueError):
calculate_tax_included_price(1000, -0.10)
def test_different_tax_rates():
"""軽減税率8%のケース"""
assert calculate_tax_included_price(1000, 0.08) == 1080
新しいテストを実行すると、バリデーションがないので異常系のテストが失敗します。実装を修正しましょう。
# tax_calculator.py(修正版)
import math
def calculate_tax_included_price(price, tax_rate):
"""税込み価格を計算する(端数切り捨て)"""
if price < 0:
raise ValueError("価格は0以上で指定してください")
if tax_rate < 0:
raise ValueError("税率は0以上で指定してください")
return math.floor(price * (1 + tax_rate))
$ pytest test_tax.py
# 6 passed
このように「テスト追加 → 失敗確認 → 実装修正 → 成功確認」のサイクルを繰り返すことで、着実に機能を積み上げていきます。
ステップ5:リファクタリングする(Refactor)
すべてのテストが通った状態で、コードの可読性・保守性を改善します。リファクタリング中もテストを実行し、壊れていないか確認しながら進めます。
# tax_calculator.py(リファクタリング後)
import math
from typing import Union
Number = Union[int, float]
def calculate_tax_included_price(price: Number, tax_rate: Number) -> int:
"""
税込み価格を計算する(端数切り捨て)。
Args:
price: 税抜き価格(0以上)
tax_rate: 税率(0以上、例: 10%なら0.10)
Returns:
税込み価格(整数)
Raises:
ValueError: price または tax_rate が負の値の場合
"""
_validate_inputs(price, tax_rate)
return math.floor(price * (1 + tax_rate))
def _validate_inputs(price: Number, tax_rate: Number) -> None:
"""入力値のバリデーション"""
if price < 0:
raise ValueError("価格は0以上で指定してください")
if tax_rate < 0:
raise ValueError("税率は0以上で指定してください")
$ pytest test_tax.py
# 6 passed
リファクタリング後もすべてのテストが通っています。コードの動作を変えずに、型ヒント・docstring・関数分割で可読性を高めました。
これが「Red - Green - Refactor」の1サイクルです。実際の開発では、このサイクルを何十回、何百回と繰り返していくことになります。
テスト駆動開発のメリット・デメリット比較
TDDを導入する前に、メリットとデメリットを正直に把握しておきましょう。
| 観点 | メリット | デメリット |
|---|---|---|
| 品質 | バグを早期に発見でき、手戻りが減る | テストコードの品質が低いと逆効果 |
| 設計 | テストしやすい=疎結合な設計に自然と向かう | 設計の全体像が見えにくくなることがある |
| スピード | 長期的には開発速度が向上する | 短期的にはテスト作成分の工数が増える |
| ドキュメント | テストが仕様書の役割を果たす | テストの保守コストが発生する |
| 心理的安全性 | リファクタリングに自信を持てる | 慣れるまでの学習コストがかかる |
結論として、TDDは長期的に開発を続けるプロジェクトほど効果が大きい手法です。短期の使い捨てスクリプトには向きませんが、チームで保守するシステムには大きな恩恵をもたらします。
TDDを成功させる3つのコツ
コツ1:最初は「小さすぎる」くらいでちょうどいい
TDD初心者がよくやる失敗は、最初から複雑な機能をまとめてテストしようとすることです。
「ユーザー登録機能」をいきなりテストするのではなく、「メールアドレスの形式チェック」「パスワードの文字数チェック」のように、1つの関数・1つの振る舞いに絞ってテストを書きましょう。
コツ2:テスト名は「日本語」で書いてもいい
テスト関数の名前を英語で書こうとして悩む時間はもったいないです。Pythonなら日本語のdocstringを活用できますし、pytest は -v オプションで関数名を表示してくれるため、テスト名自体を分かりやすくすることが重要です。
def test_empty_cart_total_is_zero():
"""カートが空のとき、合計金額は0円"""
assert calculate_total([]) == 0
コツ3:ゴールを先に定義してからテストを書く
テストを書き始める前に、「この機能は何を実現するのか」を明文化することが大切です。ゴールが曖昧なままテストを書くと、テストケースが場当たり的になり、抜け漏れが発生します。
たとえば、機能の完成条件を箇条書きでまとめてからテストに落とし込むと、網羅性が高まります。この「ゴール定義力」は、AI時代の開発においてますます重要になっています。AIにコード生成を依頼する場合でも、ゴールが明確であればあるほど、AIは精度の高いコードを返してくれるからです。
TDDとAI——テスト駆動開発はAI時代にこそ活きる
AIがコードを書く時代だからこそ、TDDの価値は高まっています。
AIにコード生成を任せるとき、「何を作るか」を明確に定義する力がなければ、AIは意図と異なるコードを返します。TDDのテストコードは、まさにこの「ゴール定義」そのものです。
実際にAIとTDDを組み合わせると、次のような開発フローが実現します。
- 人間がテストケースを書く(=ゴールを定義する)
- AIが実装コードを生成する(=ゴールに基づいて自律的に動く)
- テストを実行して検証する(=AIの成果物を客観的に評価する)
- 人間がリファクタリングを指示する(=品質をコントロールする)
このフローでは、ゴールさえしっかり定義すれば、AIが自律的にコードを生成してくれます。チャットで逐一指示を出す「対話型」ではなく、包括的なゴールを渡して任せる「自律起動型」のAI活用が可能になるのです。
TDDで培われる「ゴールを定義する力」と「成果物を評価する力」は、AIツールが変わっても通用する普遍的なスキルです。こうしたスキルを個人の暗黙知にとどめず、チームや組織の資産として蓄積していくことが、DX推進を加速させる鍵になります。
まとめ
テスト駆動開発のやり方を5ステップで振り返ります。
- テストケースを書く——まず期待する動作をテストで表現する
- 失敗を確認する——テストが正しく機能していることを検証する
- 最小限のコードを書く——テストを通すことだけに集中する
- テストケースを追加する——エッジケース・異常系を広げていく
- リファクタリングする——テストを維持しながらコードを改善する
TDDは最初こそ「遠回り」に感じますが、バグの早期発見、設計品質の向上、リファクタリングへの自信といった恩恵が積み重なり、長期的には開発速度と品質の両方を引き上げます。
まずは今日、自分のプロジェクトで「テストを1つ書いてからコードを書く」を試してみてください。その小さな一歩が、開発プロセスを変える起点になるはずです。
