こんにちは🍣🍶です。
自動テストを導入したいけど書き方が分からないなんて事ありませんか?
簡単に基本的な書き方を紹介したいと思います!
早速ですが、単純な例を挙げてみます。 よくある配列の要素の中で条件にマッチする物を返すfindで試してみます。
<?php class ArrayUtil { public static function find(array $array, callable $cb) { foreach ($array as $item) { if ($cb($item)) { return $item; } } return null; } }
こちらがテストです。
PHPUnit\Framework\TestCase;
を継承したclassのpublic
かつ、メソッド名がtest
で始まる(または@test annotationがついている)メソッドがテストとして認識され実行されます。
<?php use PHPUnit\Framework\TestCase; class ArrayUtilTest extends TestCase { public function testFindShouldReturnFoundValue() { $expected = 7; $actual = ArrayUtil::find([1, 2, 3, 4, 5, 6, 7], fn($v) => $v === 7); self::assertSame($expected, $actual); } public function testFindShouldReturnNullWhenNotFound() { self::assertSame(null, ArrayUtil::find([1, 2, 3, 4, 5, 6, 7], fn($v) => false)); } }
assertSame
の第一引数に予期される結果、第二引数に実際にテスト対象を実行した結果を渡します。
第一引数と第二引数の値が一致すれば成功、一致しなければテスト失敗となります。
このように期待される値と実際に実行した結果を渡して行くだけです
簡単ですね!
また、似たようなassertEquals
というメソッドがありますが、原則assertSame
を使うようにしましょう。
assertEquals
では暗黙的に型変換され、実際の値は違うのにテストが通ってしまうことがあるためです。
以下 assertEqualsだと通るが、assertSameだとテストが落ちる例です。
<?php self::assertEquals(null, 0); self::assertEquals(false, 0); self::assertEquals('0', 0); self::assertEquals('1', true); self::assertEquals(new \stdClass(), new \stdClass()); // テストが通ってしまう
ここからはこれってどうすればいいんだろう?
となりがちな例をいくつか上げていきます!
他のクラスのメソッドを呼び出している場合
以下のように他のクラスのメソッドを呼び出す場合はどのようにテストすれば良いでしょうか?
<?php class Foo { private Hoge $hoge; public function __construct(Hoge $hoge) { $this->hoge = $hoge; } public function bar(int $i): void { $j = $i * $i + 2; $this->hoge->piyo($j); } }
Hoge
classに関しては別のファイルでテストするべきなので、内部処理に関しては気にしたくないですよね。
なので、ここではbar
に引数x
を与えたとして
Hoge
のpiyo
が1回呼ばれる。x
とpiyo
に与えられた引数の関係が正しい
この2点が確かめられればFoo
classのテスト出来たと言えるのではないでしょうか。
ここではMockeryというテストライブラリを使ってテストを書いてみます。
<?php class FooTest extends TestCase { use Mockery; use PHPUnit\Framework\TestCase; public function testBarShouldCallPiyoWithArgs() { $hoge = Mockery::mock(Hoge::class); // Hogeのmockを作る $hoge ->shouldReceive('piyo') // piyoが呼ばれる ->once() // 呼ばれるのは一度である ->with(6); // 引数は`6`である $foobar = new Foo($hoge); $foobar->bar(2); } // tearDownは各テスト実行後に実行されるcleanup関数です。 // ここではMockery内部で行われたassertionの回数をphpunit側に伝える処理が書いてあります。 // このclassのテストとは関係ありません。 public function tearDown(): void { if ($container = Mockery::getContainer()) { $this->addToAssertionCount($container->mockery_getExpectationCount()); } Mockery::close(); } }
テストで使いたくないサービスの結果を使っている場合
外部サービスだったり長い時間がかかるサービスだったりでテストで実際に使いたくない場合って往々にしてありますよね
以下のコードでApiService
は実際に使ってはいけないものとした場合
どのようにテストすれば良いでしょうか?
<?php class Baz { private ApiService $api; public function __construct(ApiService $api) { $this->api = $api; } public function some() { $data = $this->api->hoge(); return array_reduce($data, fn($acc, $cur) => $acc + $cur['foo'], 0); } }
こんな時はスタブを使いましょう!
本物のApiService
の代わりに、テストしたい値を返すスタブを作ります。
<?php use Mockery; use PHPUnit\Framework\TestCase; class FooTest extends TestCase { public function testSomeShouldReturnsSumOfFoo() { $api = Mockery::mock(ApiService::class); // スタブを作る $api ->shouldReceive('hoge') // hogeが呼ばれる ->andReturn([ // hogeの戻り値はこう [ 'foo' => 2, 'bar' => 'a', ], [ 'foo' => 3, 'bar' => 'b', ], ]); $hoge = new Baz($api) self::assertSame(5, $hoge->some()); } }
メソッド内部でインスタンスを生成している場合
さて以下のようなケースではどうすれば良いでしょうか?
<?php class ClassA { public function method(string $a) { $api = new ApiService(); $data = $api->piyo($a); return array_map(fn($it) => $it['foo']); } }
一つの解決策はコードを以下のように書き換えてしまうことです。
上記のコードではメソッド内部でインスタンスを生成しているため、ApiServiceに依存してしまいます。
そこで引数として外部から受け取る形に変更することで、テストの際にテスト用のオブジェクトに置き換えてテスト出来るようになります。
このようなテスト用のオブジェクトをテストダブルと呼びます(モックやスタブはその一種です)
今回の場合はApiServiceを引数で受け取るようにすることでスタブを使って簡単にテスト出来るようになります。 あとは一つ上のケースと同じようにテストが書けますね!
<?php interface ApiServiceInterface { public function piyo(string $a): array } class ClassA { public function method(string $a, ApiServiceInterface $api) { // 内部でnewするのではなく引数として受け取る $data = $api->piyo($a); return array_map(fn($it) => $it['foo']); } }
このようにどんなコードでも簡単にテスト出来るわけではありません。
テストすることを意識した設計、コーディングが必要になります。
コードを書く時はテストを書くことも考えてコーディングしていきたいですね!
\\『真のユーザーファーストでマーケットを創造する』仲間を募集中です!! //