株式会社クイックのWebサービス開発blog

HAPPYなサービスプランナー・エンジニア・デザイナーのブログです。

Unit testってどう書くの?(初心者向け)[PHPUnit]

こんにちは🍣🍶です。

自動テストを導入したいけど書き方が分からないなんて事ありませんか?
簡単に基本的な書き方を紹介したいと思います!

早速ですが、単純な例を挙げてみます。 よくある配列の要素の中で条件にマッチする物を返す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);
  }
}

Hogeclassに関しては別のファイルでテストするべきなので、内部処理に関しては気にしたくないですよね。

なので、ここではbarに引数xを与えたとして

  • Hogepiyoが1回呼ばれる。
  • xpiyoに与えられた引数の関係が正しい

この2点が確かめられればFooclassのテスト出来たと言えるのではないでしょうか。

ここでは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']);
  }
}

このようにどんなコードでも簡単にテスト出来るわけではありません。
テストすることを意識した設計、コーディングが必要になります。

コードを書く時はテストを書くことも考えてコーディングしていきたいですね!


\\『真のユーザーファーストでマーケットを創造する』仲間を募集中です!! //

919.jp