😺

LaravelにおけるPHPUnitの基本的な使い方~Unitテスト編~

2024/05/17に公開

PHPUnitとは

PHPUnitとはPHP用のテストフレームワークです。Laravelには標準で入っています。
UnitテストとFeatureテストに分けることができ、Unitテストは単体テストでクラスのメソッドやプロパティをテストします。Featureテストは機能テストで定義したルートに対して、アクセスし期待する結果(ステータスコードやビュー、登録値)が返るかどうかをテストします。
どちらもartisanコマンドでベースファイルを作成することができて、作成されたファイルに対してテストケースとアサーションを必要な分だけ記述します。その後テスト実行コマンドを実行することによりテストを実行することができます。
今回は二つのテスト手法のうち、Unitテストのやり方について解説します。

Unitテスト基本の機能

Unitテストはクラスのプロパティやメソッドに関するテストになります。

サンプルコード1:テスト対象クラス

class MathService
{
    /**
     * Add two numbers.
     *
     * @param int $a
     * @param int $b
     * @return int
     */
    public function add($a, $b)
    {
        return $a + $b;
    }

    /**
     * Subtract one number from another.
     *
     * @param int $a
     * @param int $b
     * @return int
     */
    public function subtract($a, $b)
    {
        return $a - b;
    }

    /**
     * Multiply two numbers.
     *
     * @param int $a
     * @param int $b
     * @return int
     */
    public function multiply($a, $b)
    {
        return $a * $b;
    }

    /**
     * Divide one number by another.
     *
     * @param int $a
     * @param int $b
     * @return float|int
     * @throws \InvalidArgumentException
     */
    public function divide($a, $b)
    {
        if ($b == 0) {
            throw new \InvalidArgumentException('Division by zero');
        }
        return $a / $b;
    }
}

サンプルコード2:テストコード

class MathServiceTest extends TestCase
{
    protected $mathService;

    protected function setUp(): void
    {
        parent::setUp();
        $this->mathService = new MathService();
    }

    public function test_add()
    {
        $result = $this->mathService->add(2, 3);
        $this->assertEquals(5, $result);
    }

    public function test_subtract()
    {
        $result = $this->mathService->subtract(5, 3);
        $this->assertEquals(2, $result);
    }

    public function test_multiply()
    {
        $result = $this->mathService->multiply(2, 3);
        $this->assertEquals(6, $result);
    }

    public function test_divide()
    {
        $result = $this->mathService->divide(6, 3);
        $this->assertEquals(2, $result);
    }

    public function test_divide_by_zero()
    {
        $this->expectException(\InvalidArgumentException::class);
        $this->expectExceptionMessage('Division by zero');
        $this->mathService->divide(6, 0);
    }
}

テストファイル作成コマンド

以下のコマンドを打つことでtest/unitディレクトリ配下にテストファイルを作成することができます。
またファイル名をdirectoryname/xxxxTestとすることでtests/Unit以下で任意のディレクトリを切ることも可能です。

php artisan make:test MathServiceTest --unit

テスト実行コマンド

以下のコマンドを打つことによって作成したテストを実行することができます。
以下の例ではtests配下のテストが全て実行されますが、ファイル名を指定したり、グループのみのテストのみのテストを個別で実行することも可能です。

php artisan test

setUpとtearDown

setUp

protected function setUp(): void
{
    parent::setUp();
    // セットアップ処理・・・
}

setUp()メソッドは、各テストメソッドの実行前に呼び出されます。主な目的は、テストに必要な前提条件を設定することです。例えば、データベースの接続を確立したり、テスト対象のオブジェクトを生成したり、テストデータを生成することがあります。parent::setUp();は必ず必要。
また、各テストメソッド前ではなく、テストクラスの開始時に一度だけ実行されるsetUpBeforeClass()メソッドもあります。

teaDown

protected function tearDown(): void
{
    // クリーンアップ処理・・・
    parent::tearDown();
}

tearDown()メソッドは、各テストメソッドの実行後に呼び出されます。主な目的は、テストが影響を及ぼした状態をクリーンアップすることです。例えば、テストで作成されたデータを削除したり、リソースを解放したりすることがあります。
また、各テストメソッド後ではなく、テストクラスの終了時に一度だけ実行されるtearDownAfterClass()メソッドもあります。
※サンプルには記述なし、必要に応じて追加してください

assert

$this->assertEquals(5, $result);

アサーションとはテストが期待値通りかどうかを確認するためのメソッドです。代表的なものは以下のものが挙げられます。

  • assertEquals():期待される値と実際の値が等しいことをアサートします。
  • assertTrue(), assertFalse():条件が真であること、または偽であることをアサートします。
  • assertNull(), assertNotNull():値がnullであることまたはnullでないことをアサートします。
  • assertSame():2 つの値が同じオブジェクトを参照していることをアサートします。

※詳しくは公式ドキュメントへ
https://docs.phpunit.de/en/9.6/assertions.html

例外時のテスト

expectException メソッドは、指定された例外が発生することをテストするために使用されます。引数としては、期待される例外のクラス名を文字列で渡すか、クラス名を直接指定します。
expectExceptionMessage メソッドを使用すると、発生する例外のメッセージもアサートできます。

$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Division by zero');

データプロバイダ

サンプルコード4:テストコード(テスト対象のコードは変更無し)

class MathServiceTest extends TestCase
{
    protected $mathService;

    protected function setUp(): void
    {
        parent::setUp();
        $this->mathService = new MathService();
    }

    /**
     * @dataProvider additionDataProvider
     */
    public function test_add($a, $b, $expected)
    {
        $result = $this->mathService->add($a, $b);
        $this->assertEquals($expected, $result);
    }

    public static function additionDataProvider()
    {
        return [
            [2, 3, 5],
            [5, 3, 8],
            [0, 0, 0],
            [-2, 3, 1],
        ];
    }

    /**
     * @dataProvider subtractionDataProvider
     */
    public function test_subtract($a, $b, $expected)
    {
        $result = $this->mathService->subtract($a, $b);
        $this->assertEquals($expected, $result);
    }

    public static function subtractionDataProvider()
    {
        return [
            [5, 3, 2],
            [5, 5, 0],
            [0, 0, 0],
            [-2, 3, -5],
        ];
    }
}

データプロバイダの定義方法

データプロバイダは、PHPUnitにおいて、同じテストメソッドを異なるデータセットで複数回実行するための仕組みです。特定のテストメソッドに対して、複数のテストケースやパラメータセットを定義し、それぞれのデータセットでテストを実行することができます。
定義したデータはテストメソッドの引数にて受け取ることができ、そのデータを用いてテストを行います。
また、コメントで@dataProvider 用いるプロバイダ名をつける必要があります。

テストダブル(モックとスタブ)

サンプルコード5:テストコード

class MathServiceTest extends TestCase
{
    public function test_add_with_stub()
    {
        // Logger クラスのスタブを作成
        $loggerStub = $this->createStub(Logger::class);

        // スタブの log メソッドがメッセージを返すように設定
        $loggerStub->method('log')->willReturn('Message from logger');

        // MathService クラスのインスタンスを作成し、Logger クラスのスタブを注入
        $mathService = new MathService($loggerStub);

        // add メソッドを呼び出してテスト
        $result = $mathService->add(3, 5);

        // 期待される結果は 8 である
        $this->assertEquals(8, $result);
    }

    public function test_add_with_mock()
    {
        // Logger クラスのモックを作成
        $loggerMock = $this->getMockBuilder(Logger::class)
            ->getMock();

        // モックの log メソッドがメッセージを返すように設定
        $loggerMock->expects($this->once())
            ->method('log')
            ->willReturn('Message from logger');

        // MathService クラスのインスタンスを作成し、Logger クラスのモックを注入
        $mathService = new MathService($loggerMock);

        // add メソッドを呼び出してテスト
        $result = $mathService->add(3, 5);

        // 期待される結果は 8 である
        $this->assertEquals(8, $result);
    }
}  

サンプルコード6:テスト対象コード

class MathService
{
    protected $logger;

    public function __construct(Logger $logger)
    {
        $this->logger = $logger;
    }

    public function add($a, $b)
    {
        $this->logger->log("Adding $a and $b");
        return $a + $b;
    }
}

サンプルコード7:依存クラス

class Logger
{
    public function log()
    {
        return '文字列';
    }
}

class MathServiceTest extends TestCase
{
    public function testPrivateModMethod()
    {
        $value = 7;
        $mathService = new MathService();
        
        // ReflectionClassを使ってprivateメソッドにアクセスする
        $reflection = new ReflectionClass($mathService);
        $method = $reflection->getMethod('add');
        $method->setAccessible(true);

        // privateメソッドを呼び出す
       $method->invokeArgs($mathService, [&$value]);

        // 結果をアサート
        $this->assertEquals(1, $value);

        // privateプロパティの値を再度取得してアサート
        $this->assertEquals(7, $mathService->getValue());
    }
}

テストダブルは、テスト中に使用されるオブジェクトの代役として機能します。
今回の例では、MathServiceクラスがLoggerクラスに依存している例です。
現在テストしたいのはMathServiceクラスですので、Loggerクラスはモック、スタブとして代役させます。

  • モック:モックは、テスト中にオブジェクトの振る舞いを擬似的に定義し、その振る舞いがテスト中に期待通りに行われていることを検証します。モックは通常、特定のメソッドが呼び出されることや、呼び出し回数、引数などを追跡および検証します。
  • スタブ:スタブは、テスト中にオブジェクトの特定のメソッドを置き換え、そのメソッドが期待されるように動作するようにします。スタブは、通常、メソッドが呼び出された際に事前に設定された値や動作を返します。

モックの作り方

モックの作り方はいくつかありますが今回はgetMockBuilderメソッドを使って行いました。引数にクラス名を入れることでそのクラスのモックを作成することができます。
->method('log')でモックするメソッド名、->expects($this->once())で呼び出し回数、->willReturn('Message from logger')で返り値を設定できます。

// Logger クラスのモックを作成
$loggerMock = $this->getMockBuilder(Logger::class)
    ->getMock();

// モックの log メソッドがメッセージを返すように設定
$loggerMock->expects($this->once())
    ->method('log')
    ->willReturn('Message from logger');

// MathService クラスのインスタンスを作成し、Logger クラスのモックを注入
$mathService = new MathService($loggerMock);

スタブの作り方

スタブはcreateStubメソッドで作成します。引数にクラス名を入れることでそのクラスのスタブを作成することができます。メソッドと返り値についてはモックと同様なので省略。

// Logger クラスのスタブを作成
$loggerStub = $this->createStub(Logger::class);

// スタブの log メソッドがメッセージを返すように設定
$loggerStub->method('log')->willReturn('Message from logger');

non-publicメソッドのテスト

privateメソッドはクラスをインスタンス化させて、直接実行することができません。
そのため、reflectionClassを用いてテストを行うことができる

サンプルコード8:テストコード

class MathServiceTest extends TestCase
{
    public function testPrivateModMethod()
    {
        $mathService = new MathService();
        
        // ReflectionClassを使ってprivateプロパティにアクセスする
        $reflection = new ReflectionClass($mathService);
        $property = $reflection->getProperty('value');
        $property->setAccessible(true);
        $property->setValue($mathService, 7);

        // ReflectionClassを使ってprivateメソッドにアクセスする
        $method = $reflection->getMethod('mod');
        $method->setAccessible(true);

        // privateメソッドを呼び出す
        $result = $method->invokeArgs($mathService, [3]);

        // 結果をアサート
        $this->assertEquals(1, $result);

        // privateプロパティの値を再度取得してアサート
        $value = $property->getValue($mathService);
        $this->assertEquals(7, $value);
    }
}

サンプルコード9:テスト対象コード

※セッターがない変なクラスですが、サンプル用としてご了承を、、

class MathService
{
    private $value;

    private function mod($b)
    {
        return $this->value % $b;
    }

    public function getValue()
    {
        return $this->value;
    }

    // apiなどからデータを取得し$valueにセットするメソッドがある
}

プロパティに値をセットする

$reflection = new ReflectionClass($mathService);
$property = $reflection->getProperty('value');
$property->setAccessible(true);
$property->setValue($mathService, 7);

まずはReflectionClassをインスタンス化させます。引数はテスト対象のクラスです。
その後getPropertyメソッドでプロパティを取得し、setAccessibleメソッドでアクセス許可をします。
最後にsetValueメソッドで入れたい値をセットすることでpublicではないプロパティに値をセットできます。

メソッドを実行する

$method = $reflection->getMethod('mod');
$method->setAccessible(true);

// privateメソッドを呼び出す
$result = $method->invokeArgs($mathService, [3]);

メソッドも同様にgetMethodメソッドを用いてメソッドを取得した後に、アクセスを許可します。
その後、invokeArgsメソッドを用いることで取得したメソッドを実行しています。
Argsとついているように、第二引数ではメソッドの引数を配列で指定することができます。
後の流れは他のテストケース同様です。

例外編:参照渡しの場合

メソッドの引数に参照わたしが使われている場合があると思います。その場合はinvokeArgsメソッドの第二引数の配列にも&を追加して参照を渡してあげる必要があります。

class MathService
{
    private function add(&$b)
    {
        $b = $b + 1;
    }
}

class MathServiceTest extends TestCase
{
    public function testPrivateModMethod()
    {
        $value = 1;
        $mathService = new MathService();
        
        $reflection = new ReflectionClass($mathService);
        $method = $reflection->getMethod('add');
        $method->setAccessible(true);

        // 引数に&をつけて渡す
        $method->invokeArgs($mathService, [&$value]);

        // 結果をアサート($valueが1増加して2になる)
        $this->assertEquals(2, $value);
    }
}

終わり

今回はPHPUnitのUnitテストについての基本的なやり方について記述しました。
参考になれば幸いです!

Discussion