テスト駆動開発のワークフロー
レッド・グリーン・リファクタリングのサイクルで設計を進めます。
「テスト駆動開発のワークフロー」はCoddyKit上の無料PHP Academyレッスンです。 これはレッスン1/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはPHP Academy学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 PHP Academyコースには全4レッスンが含まれています。
TDDは設計のためのツール
テスト駆動開発はテスト手法として紹介されることが多いですが、その本当の価値は設計への圧力にあります。先にテストを書くことで、実装を書く前に、オブジェクトの公開インターフェース、依存関係、契約を定義せざるを得なくなります。テストは副産物であり、成果物はより良い設計です。
Red、Green、Refactor
このサイクルには、厳密な3つの段階があります。
- Red — 次の小さな振る舞いに対する失敗するテストを書きます。正しい理由で失敗しなければなりません。
- Green — たとえ見栄えが悪くても、テストを通すための最小限のコードを書きます。
- Refactor — Greenの状態を保ちながら、実装とテストを整理します。
規律が重要です。Redを決して省略しないでください。何もテストしていない可能性があるためです。また、Greenで作り込みすぎないでください。
Red:先に失敗するテストを書く
パーセンテージ割引を適用するPriceCalculatorを作ります。まず、まだ存在しない振る舞いのテストを書きます。実行するとクラスが未定義であるため失敗します。これは正しいRedです。
<?php
use PHPUnit\Framework\TestCase;
final class PriceCalculatorTest extends TestCase
{
public function test_applies_a_percentage_discount(): void
{
$calc = new PriceCalculator();
// 100 with 10% off -> 90.0
self::assertSame(90.0, $calc->withDiscount(100.0, 10));
}
}
Green:通すための最小限のコード
次に、テストをGreenにする最小限の実装を書きます。丸め規則、バリデーション、通貨処理を追加したくなっても我慢してください。それらを要求する失敗テストは、まだ存在しません。
<?php
final class PriceCalculator
{
public function withDiscount(float $amount, float $percent): float
{
return $amount - ($amount * $percent / 100);
}
}
$calc = new PriceCalculator();
var_dump($calc->withDiscount(100.0, 10)); // float(90)
一般化を促すために三角測量する
テストが1つだけだと、ハードコードでも通せてしまいます。三角測量、つまり異なる2つ目の例を追加することで、実装を一般化せざるを得なくなります。単純な解決策ではごまかせないケースを追加し、本当の計算式を実装させます。
<?php
use PHPUnit\Framework\TestCase;
final class PriceCalculatorTest extends TestCase
{
public function test_zero_percent_is_unchanged(): void
{
self::assertSame(50.0, (new PriceCalculator())->withDiscount(50.0, 0));
}
public function test_full_discount_is_free(): void
{
self::assertSame(0.0, (new PriceCalculator())->withDiscount(50.0, 100));
}
}
エッジケースを仕様として駆動する
TDDでは、新しい要件は新しい失敗テストです。たとえば、負の割引率を無効とします。まず例外が発生することを期待するテストを書きます。まだ何も例外をスローしないため、テストは失敗します。このテストが契約を文書化します。
<?php
use PHPUnit\Framework\TestCase;
final class PriceCalculatorValidationTest extends TestCase
{
public function test_rejects_negative_discount(): void
{
$this->expectException(\InvalidArgumentException::class);
(new PriceCalculator())->withDiscount(100.0, -5);
}
}
再びGreenにしてからRefactorする
新しいテストを通すためのガードを追加し、既存のテストが退行を検出してくれるという確信を持ってリファクタリングします。丸め処理も、テストまたは明確な要件によって正当化されてから一度だけ導入したことに注目してください。
<?php
final class PriceCalculator
{
public function withDiscount(float $amount, float $percent): float
{
if ($percent < 0 || $percent > 100) {
throw new \InvalidArgumentException('percent must be 0..100');
}
return round($amount - ($amount * $percent / 100), 2);
}
}
var_dump((new PriceCalculator())->withDiscount(19.99, 15)); // float(16.99)
データプロバイダーで例をまとめる
振る舞いが安定したら、多数の例の組み合わせを、データプロバイダーを使った1つのパラメーター化テストにまとめます。これにより、Red/Greenループを速く保ちつつ、意図を表形式で読みやすくできます。
<?php
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Attributes\DataProvider;
final class DiscountTableTest extends TestCase
{
#[DataProvider('cases')]
public function test_discounts(float $amount, float $pct, float $expected): void
{
self::assertSame($expected, (new PriceCalculator())->withDiscount($amount, $pct));
}
public static function cases(): array
{
return [
'no discount' => [100.0, 0, 100.0],
'ten percent' => [100.0, 10, 90.0],
'free' => [100.0, 100, 0.0],
];
}
}
ループを速く保つ
TDDが機能するのは、フィードバックループが数分ではなく数秒で回る場合だけです。実践的な方法は次のとおりです。
- 単一のファイルを実行するか、フィルターを使います:
phpunit --filter test_full_discount_is_free。 --testdoxを使い、テストを振る舞いの仕様として読みます。- ユニットテストからI/Oを排除します。内側のループではDB、ネットワーク、ファイルシステムを使いません。
vendor/bin/phpunit --testdox --filter PriceCalculatorテストもリファクタリングする
Refactorの段階で対象となるのは本番コードだけではなく、テストスイートも含まれます。ヘルパーやsetUp()で重複を取り除き、振る舞いに基づいてテストに名前を付け、意味のあることを何もアサートしなくなったテストは削除します。テストも将来にわたって保守するコードです。実装と同じように大切に扱ってください。
<?php
use PHPUnit\Framework\TestCase;
final class PriceCalculatorTest extends TestCase
{
private PriceCalculator $calc;
protected function setUp(): void
{
$this->calc = new PriceCalculator(); // shared setup, no duplication
}
public function test_ten_percent(): void
{
self::assertSame(90.0, $this->calc->withDiscount(100.0, 10));
}
}
TDDで依存関係を設計する
先にテストを書くため、扱いにくい依存関係がすぐに明らかになります。テストでインスタンス化しにくいクラスがあるなら、それは設計に関するフィードバックです。内部でnewするのではなく、協調オブジェクトを注入してください。以下の割引計算機は、丸め方針をハードコードせずに受け取ることでテスト可能になります。TDDによって、その差し替えポイントが生まれたのです。
<?php
interface RoundingPolicy { public function round(float $v): float; }
final class PriceCalculator
{
public function __construct(private RoundingPolicy $rounding) {}
public function withDiscount(float $amount, float $percent): float
{
return $this->rounding->round($amount - ($amount * $percent / 100));
}
}
// Tests inject a deterministic RoundingPolicy; no hidden global behavior.
確認問題
Redの段階の目的は何ですか。
まとめ
設計規律としてTDDを実践しました。
- Red-Green-Refactor:失敗するテスト、最小限のコード、そしてGreenの状態での整理です。
- 三角測量によって一般化が促され、新しい要件は新しい失敗テストとして追加されます。
- データプロバイダーで安定したケースをまとめ、
--filter/--testdoxでループを速く保ちます。 - テストもリファクタリングしてください。テストも長期にわたって保守するコードです。
次はMockeryを使った柔軟なテストダブルです。
よくある質問
「テスト駆動開発のワークフロー」レッスンは無料ですか?
はい。「テスト駆動開発のワークフロー」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、PHP Academyコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 PHP Academyコースには全4レッスンが含まれています。
「テスト駆動開発のワークフロー」で何を学びますか?
レッド・グリーン・リファクタリングのサイクルで設計を進めます。 ブラウザで直接実行するハンズオンコードでPHP Academyを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。
PHP Academyを始めるのに経験は必要ですか?
事前経験は必要ありません。CoddyKitのPHP Academyは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン1/4です。
「テスト駆動開発のワークフロー」レッスンにはどのくらい時間がかかりますか?
ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。
このPHP Academyレッスンでコードを書いて実行できますか?
はい。すべてのPHP Academyレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。
このコースのすべてのレッスン
- テスト駆動開発のワークフロー
- Mockeryによるモックとスタブ
- 統合テストと機能テスト
- Infectionによるミューテーションテスト