- テストを書く
- コンパイラを通す(インタプリタ言語の場合は、静的解析など?)
- テストを走らせ、失敗することを確認する
- テストを通す
- 重複を排除する
- PHP 8.2.7 (cli)
- Composer version 2.5.8
composerを使ってインストール。
comopser.json
{
"require-dev": {
"phpunit/phpunit": "10.0.*"
}
}$ composer installapp/とtest/を作成
$ tree -L 1
.
├── README.md
├── app
├── composer.json
├── composer.lock
├── tests
├── todo.md
└── vendor
composer.jsonに以下を追記
{
"autoload": {
"psr-4": {
"App\\": "app/"
}
}
}以下を実行
$ composer dump-autoload/testsにテストファイル、/appにテスト対象のファイルを作成し、以下を実行
$ ./vendor/bin/phpunit tests --color- オブジェクトを値として使う(参照との対比)
- 別名参照を気にする必要がなくなる
3章の終了時点では、テストする時にDollarのamountにアクセスする必要があった。 これは実装の詳細なので、private変数にすることで、外部から隠蔽する。そうすることで、テストコードがDollarの内部実装に依存しなくなる。
まずは、テストを書いて、テストを通すように実装するのがTDDの手順なので、重複したコードがあっても良い。 後で、重複を排除する。
Fatal error: Cannot declare class Money, because the name is already in use in /path_to_dir/tdd-practice/app/Money.php on line 3 の解消
原因:namespaceの書き忘れ
<?php
namespace App; // これを書かないとエラーになる
class Money {
}重複を消すために、DollarとFrancのtimesメソッドを一致させる。(完全に一致させられれば、Moneyクラスに移動できる) シグネチャを合わせたり、Moneyクラスをabstractにしてtimesメソッドを宣言したりして、徐々に近づけていく。 その間も、常にテストが通ることを確認しながら進める。
各々のコンストラクタの実装を合わせて、Moneyクラスに移動した。
Money.php
public function __construct(int $amount, string $currency)
{
$this->amount = $amount;
$this->currency = $currency;
}Dollar.php
public function __construct(int $amount, string $currency)
{
parent::__construct($amount, $currency);
}timesをMoneyクラスに移動するために、各々の子クラスでtimesメソッドの実装を合わせる。
- timesメソッドの中でMoneyをインスタンス化する
- Moneyをインスタンス化するために、Moneyクラスを具象クラスにする
- Moneyクラスを具象クラスにするためには、timesのabstractを削除する
- timesは各々の子クラスで実装しているため、Moneyクラスでは仮実装で良い(オーバーロードされる)
public function times(int $multiplier): Money { return new Money(0, ''); }
junitでは、プロダクションコードのequalsメソッドを使って、テスト時のオブジェクトの等価性を比較している(?) phpunitでは、特にそういう機能はないので、型があっていないオブジェクトの等価性比較をすると、falseになる。
toStringもおそらくjunitのみだと思われる。
phpunitでも、対応するようなものがないか調べる。
サブクラスを削除したことによって、不要になったテストを削除した。 そのテストが不要になったかどうかは、それぞれが何を検証しているかを考慮して判断する。
テストが整備されていると、新しいアイデアを試しやすくなる。
p.94 の中断あたりで、Sumにreduceを持ってくると、グリーンバーになると書いてあるが、実際はprotectedプロパティにアクセスしてしまっているため、グリーンにならなかった。JavaとPHPの違いだろうか?
(とりあえず、不本意ながらpublicにして先に進む)
Javaのprotectedは同一パッケージであればアクセスできる仕様らしい。今回は、MoneyのamountにSumからアクセスさせたいため、phpではpublicで、妥当であると考えられるのでそのまま進むことにする。
PHPは配列のキーにオブジェクトを入れることはできないので、仕方なく通貨ペアのオブジェクトをシリアライズしたものをキーとして値を保存した。 Piarの等価性比較をしないので、ハッシュコードなどはいらないかもしれない…(次章以降で確認する)
set側
public function addRate(string $from, string $to, int $rate): void
{
$this->rates[serialize(new Pair($from, $to))] = $rate;
}get側
public function rate(string $from, string $to): int
{
if ($from === $to) return 1;
return $this->rates[serialize(new Pair($from, $to))];
}- テストを綺麗に機能させるには、仮実装、三角測量、明白な実装の方法がある
tests/TriangulationSampleTest.php --color を参照
ベタ書きでテストを通す。テストを通した後に、テストケースを追加して本物の実装を追加する。 テストケースを1+3のものだけを用意し、4を返すようにすればテストは通る。
final class SampleTest extends TestCase
{
public function testSum():void
{
$this->assertSame(4, $this->plus(3,1));
}
private function plus(int $augend, int $addend): int
{
return 4; // 仮実装
}
}2つ以上の例があるときのみ、一般化を行う。
Step1: 一つの例しかないので、一般化は行わない。(仮実装と同じ)
final class SampleTest extends TestCase
{
public function testSum():void
{
$this->assertSame(4, $this->plus(3,1));
}
private function plus(int $augend, int $addend): int
{
return 4; // 仮実装のまま
}
}Step2: 2つの例があるので、一般化を行う。
final class SampleTest extends TestCase
{
public function testSum():void
{
$this->assertSame(4, $this->plus(3,1));
$this->assertSame(7, $this->plus(3,4));
}
private function plus(int $augend, int $addend): int
{
return $augend + $addend; // 一般化
}
}
シンプルな操作をそのまま実装すること。上記のplusメソッドのようなシンプルな実装では、仮実装や三角測量をする必要は基本的にない。レッドバーが出て驚いた時などは、仮実装などのスモールステップに戻るのが良い。
仮実装などの中間地点は、あくまで手段なので、すぐに書けそうなら明白な実装にする。
まず、単純なテストとして、あるメソッドが呼ばれたがどうかを検証するテストを書く。(書籍にはpythonで書かれているが、phpで書いてみる)
WasRunというクラスを作成して、メソッドが呼ばれたかどうかをフラグを使って返すようにする。 メソッド名については、WasRunのインスタンス化時に名前を指定できるようにして、呼ぶときにはvariable functionを使った。
途中で、WasRunクラスが、「メソッドが起動されたか記録する仕事」と「テストメソッドを動的に呼び出す仕事」をするようになったので、後者の仕事をTestCaseクラスに分離した。
xunit/xunit.phpを作り、以下のコマンドで実行する。
php xunit/xunit.php一章終了時のコードは以下の通り。
<?php
declare(strict_types=1);
$testCaseTest = new TestCaseTest('testRunning');
$testCaseTest->run();
class TestCase
{
public $name;
public function __construct($name)
{
$this->name = $name;
}
public function run()
{
$func = $this->name;
$this->$func();
}
}
class WasRun extends TestCase
{
public $wasRun;
public function __construct($name)
{
$this->wasRun = null;
parent::__construct($name);
}
public function testMethod()
{
$this->wasRun = 1;
}
}
class TestCaseTest extends TestCase
{
public function testRunning()
{
$test = new WasRun('testMethod');
assert(!$test->wasRun);
$test->run();
assert($test->wasRun);
}
}
<?php
declare(strict_types=1);
(new TestCaseTest('testRunning'))->run();
(new TestCaseTest('testSetUp'))->run();
class TestCase
{
public string $name;
public ?int $wasRun;
public ?int $wasSetUp;
public function __construct(string $name)
{
$this->name = $name;
}
public function setUp(): void
{}
public function run(): void
{
$this->setUp();
$func = $this->name;
$this->$func();
}
}
class WasRun extends TestCase
{
public function setUp(): void
{
$this->wasRun = null;
$this->wasSetUp = 1;
}
public function testMethod(): void
{
$this->wasRun = 1;
}
}
class TestCaseTest extends TestCase
{
public WasRun $test;
public function setUp(): void
{
$this->test = new WasRun('testMethod');
}
public function testRunning(): void
{
$this->test->run();
assert($this->test->wasRun === 1);
}
public function testSetUp(): void
{
$this->test->run();
assert($this->test->wasSetUp === 1);
}
}
なんとWasRunのインスタンスを使う部分は1つになってしまった。気の利いたsetUpメソッドだったが、元に戻そう。
この部分の意味がわからなかった。 →使う部分が1つなので、testTemplateMethodの中でWasRunをインスタンス化すれば良いということだった。
class TestCaseTest extends TestCase
{
- public WasRun $test;
-
- public function setUp(): void
- {
- $this->test = new WasRun('testMethod');
- }
-
public function testTemplateMethod(): void
{
- $this->test->run();
- assert($this->test->log === 'setUp testMethod ');
+ $test = new WasRun('testMethod');
+ $test->run();
+ assert($test->log === 'setUp testMethod ');
}
}
<?php
declare(strict_types=1);
(new TestCaseTest('testTemplateMethod'))->run();
class TestCase
{
public string $name;
public ?int $wasRun;
public ?int $wasSetUp;
public function __construct(string $name)
{
$this->name = $name;
}
public function setUp(): void
{
}
public function tearDown(): void
{
}
public function run(): void
{
$this->setUp();
$func = $this->name;
$this->$func();
$this->tearDown();
}
}
class WasRun extends TestCase
{
public string $log;
public function setUp(): void
{
$this->log = 'setUp ';
}
public function testMethod(): void
{
$this->log = $this->log . 'testMethod ';
}
public function tearDown(): void
{
$this->log = $this->log . 'tearDown ';
}
}
class TestCaseTest extends TestCase
{
public function testTemplateMethod(): void
{
$test = new WasRun('testMethod');
$test->run();
assert($test->log === 'setUp testMethod tearDown ');
}
}
終了前に、テストを一度コメントアウトして、棚上げした。より小さいテストを追加する。
<?php
declare(strict_types=1);
(new TestCaseTest('testTemplateMethod'))->run();
(new TestCaseTest('testResult'))->run();
// (new TestCaseTest('testBrokenResult'))->run();
class TestCase
{
public string $name;
public ?int $wasRun;
public ?int $wasSetUp;
public function __construct(string $name)
{
$this->name = $name;
}
public function setUp(): void
{
}
public function tearDown(): void
{
}
public function run(): TestResult
{
$result = new TestResult();
$result->testStarted();
$this->setUp();
$func = $this->name;
$this->$func();
$this->tearDown();
return $result;
}
}
class WasRun extends TestCase
{
public string $log;
public function setUp(): void
{
$this->log = 'setUp ';
}
public function testMethod(): void
{
$this->log = $this->log . 'testMethod ';
}
public function testBrokenMethod(): void
{
throw new Exception();
}
public function tearDown(): void
{
$this->log = $this->log . 'tearDown ';
}
}
class TestResult
{
public int $runCount;
public function __construct()
{
$this->runCount = 0;
}
public function testStarted(): void
{
$this->runCount++;
}
public function summary(): string
{
return sprintf('%b run, 0 faild',$this->runCount);
}
}
class TestCaseTest extends TestCase
{
public function testTemplateMethod(): void
{
$test = new WasRun('testMethod');
$test->run();
assert($test->log === 'setUp testMethod tearDown ');
}
public function testResult(): void
{
$test = new WasRun('testMethod');
$result = $test->run();
assert($result->summary() === '1 run, 0 faild');
}
public function testFaildResult(): void
{
$test = new WasRun('testBrokenMethod');
$result = $test->run();
assert($result->summary() === '1 run, 1 faild');
}
}
<?php
declare(strict_types=1);
echo (new TestCaseTest('testTemplateMethod'))->run()->summary() . PHP_EOL;
echo (new TestCaseTest('testResult'))->run()->summary() . PHP_EOL;
echo (new TestCaseTest('testFailedResult'))->run()->summary() . PHP_EOL;
echo (new TestCaseTest('testFailedTestFormatting'))->run()->summary() . PHP_EOL;
class TestCase
{
public string $name;
public ?int $wasRun;
public ?int $wasSetUp;
public function __construct(string $name)
{
$this->name = $name;
}
public function setUp(): void
{
}
public function tearDown(): void
{
}
public function run(): TestResult
{
$result = new TestResult();
$result->testStarted();
$this->setUp();
try {
$func = $this->name;
$this->$func();
} catch (Exception $e) {
$result->testFailed();
}
$this->tearDown();
return $result;
}
}
class WasRun extends TestCase
{
public string $log;
public function setUp(): void
{
$this->log = 'setUp ';
}
public function testMethod(): void
{
$this->log = $this->log . 'testMethod ';
}
public function testBrokenMethod(): void
{
throw new Exception();
}
public function tearDown(): void
{
$this->log = $this->log . 'tearDown ';
}
}
class TestResult
{
public int $runCount;
public int $errorCount;
public function __construct()
{
$this->runCount = 0;
$this->errorCount = 0;
}
public function testStarted(): void
{
$this->runCount++;
}
public function testFailed(): void
{
$this->errorCount++;
}
public function summary(): string
{
return sprintf('%b run, %b failed', $this->runCount, $this->errorCount);
}
}
class TestCaseTest extends TestCase
{
public function testTemplateMethod(): void
{
$test = new WasRun('testMethod');
$test->run();
assert($test->log === 'setUp testMethod tearDown ');
}
public function testResult(): void
{
$test = new WasRun('testMethod');
$result = $test->run();
assert($result->summary() === '1 run, 0 failed');
}
public function testFailedResult(): void
{
$test = new WasRun('testBrokenMethod');
$result = $test->run();
assert($result->summary() === '1 run, 1 failed');
}
public function testFailedTestFormatting(): void
{
$result = new TestResult();
$result->testStarted();
$result->testFailed();
assert($result->summary() === '1 run, 1 failed');
}
}
一つひとつのテストを逐次呼んでいて、汚い実装になっているのでスイートにまとめる。
処理のパラメータに、結果格納用のオブジェクトを渡すパターン。
<?php
declare(strict_types=1);
$suite = new TestSuite();
$suite->add(new TestCaseTest('testTemplateMethod'));
$suite->add(new TestCaseTest('testResult'));
$suite->add(new TestCaseTest('testFailedResult'));
$suite->add(new TestCaseTest('testFailedResultFormatting'));
$suite->add(new TestCaseTest('testSuite'));
$result = new TestResult();
$suite->run($result);
echo $result->summary() . PHP_EOL;
class TestCase
{
public string $name;
public function __construct(string $name)
{
$this->name = $name;
}
public function setUp(): void
{
}
public function tearDown(): void
{
}
public function run(TestResult $result): void
{
$result->testStarted();
$this->setUp();
try {
$func = $this->name;
$this->$func();
} catch (Exception $e) {
$result->testFailed();
}
$this->tearDown();
}
}
class WasRun extends TestCase
{
public string $log;
public function setUp(): void
{
$this->log = 'setUp ';
}
public function testMethod(): void
{
$this->log = $this->log . 'testMethod ';
}
public function testBrokenMethod(): void
{
throw new Exception();
}
public function tearDown(): void
{
$this->log = $this->log . 'tearDown ';
}
}
class TestResult
{
public int $runCount;
public int $errorCount;
public function __construct()
{
$this->runCount = 0;
$this->errorCount = 0;
}
public function testStarted(): void
{
$this->runCount++;
}
public function testFailed(): void
{
$this->errorCount++;
}
public function summary(): string
{
return sprintf('%d run, %d failed', $this->runCount, $this->errorCount);
}
}
class TestSuite
{
/** @var TestCase[] */
public $tests;
public function __construct()
{
$this->tests = [];
}
public function add(TestCase $test): void
{
$this->tests[] = $test;
}
public function run(TestResult $result): void
{
foreach ($this->tests as $test) {
$test->run($result);
}
}
}
class TestCaseTest extends TestCase
{
public TestResult $result;
public function setUp(): void
{
$this->result = new TestResult();
}
public function testTemplateMethod(): void
{
$test = new WasRun('testMethod');
$test->run($this->result);
assert($test->log === 'setUp testMethod tearDown ');
}
public function testResult(): void
{
$test = new WasRun('testMethod');
$test->run($this->result);
assert($this->result->summary() === '1 run, 0 failed');
}
public function testFailedResult(): void
{
$test = new WasRun('testBrokenMethod');
$test->run($this->result);
assert($this->result->summary() === '1 run, 1 failed');
}
public function testFailedResultFormatting(): void
{
$this->result->testStarted();
$this->result->testFailed();
assert($this->result->summary() === '1 run, 1 failed');
}
public function testSuite(): void
{
$suite = new TestSuite();
$suite->add(new WasRun('testMethod'));
$suite->add(new WasRun('testBrokenMethod'));
$suite->run($this->result);
assert($this->result->summary() === '2 run, 1 failed');
}
}