PHP入門:PHPUnitでテストの依存関係を設定する

スポンサーリンク
PHP入門:PHPUnitでテストの依存関係を設定する ノウハウ
PHP入門:PHPUnitでテストの依存関係を設定する
この記事は約15分で読めます。
よっしー
よっしー

こんにちは。よっしーです(^^)

今日は、PHPUnitでテストの依存関係を設定する方法についてご紹介します。

スポンサーリンク

前提

この記事は下記の記事をベースにしています。

背景

ユニットテストは主に、開発者がバグを特定し修正するのを助け、コードをリファクタリングし、テスト中のソフトウェアユニットのドキュメントとして機能するための、グッドプラクティスとして書かれます。

これらのメリットを達成するために、ユニットテストは、理想的には、プログラム内のすべての可能なパスをカバーする必要があります。通常、1つのユニットテストは、1つの関数やメソッドにおける1つの特定のパスをカバーします。

しかし、テストメソッドは、必ずしもカプセル化された独立した実体ではありません。多くの場合、テストの実装シナリオの中に、テストメソッド間の暗黙の依存関係が隠されています。

PHPUnit は、テストメソッド間の明示的な依存関係の宣言をサポートしています。このような依存性によってテストメソッドの実行順が定義されるわけではありませんが、 プロデューサがテストフィクスチャのインスタンスを返し、 それを依存するコンシューマに渡すことができるようになります。

プロデューサーとは、テスト対象のユニットを返り値として返すテストメソッドです。

コンシューマーは、1 つ以上のプロデューサーとその返り値に依存するテストメソッドです。

この記事では、PHPUnitFramework の Attributes 属性を使用してテストメソッド間の依存関係を表す方法を示します。

修正内容

下記のファイルを更新、もしくは、作成します。下記の各セクションに各ファイルの修正内容を記載しています。

        new file:   19_learn_phpunit/tests/StackTest.php

tests/StackTest.php

下記の内容で新規作成します。

<?php declare(strict_types=1);
use PHPUnit\Framework\Attributes\Depends;
use PHPUnit\Framework\TestCase;

final class StackTest extends TestCase
{
    public function testEmpty(): array
    {
        $stack = [];
        $this->assertEmpty($stack);

        return $stack;
    }

    #[Depends('testEmpty')]
    public function testPush(array $stack): array
    {
        $stack[] = 'foo';
        $this->assertSame('foo', $stack[count($stack) - 1]);
        $this->assertNotEmpty($stack);

        return $stack;
    }

    #[Depends('testPush')]
    public function testPop(array $stack): void
    {
        $this->assertSame('foo', array_pop($stack));
        $this->assertEmpty($stack);
    }
}

動作確認

下記のコマンドを実施します。

docker compose run -it --rm php-cmd ./vendor/bin/phpunit tests/StackTest.php

下記の結果になれば成功です。

% docker compose run -it --rm php-cmd ./vendor/bin/phpunit tests/StackTest.php
[+] Building 0.0s (0/0)                                                                                                                            
[+] Building 0.0s (0/0)                                                                                                                            
PHPUnit 10.2.6 by Sebastian Bergmann and contributors.

Runtime:       PHP 8.1.12-1ubuntu4.2

...                                                                 3 / 3 (100%)

Time: 00:00.073, Memory: 6.00 MB

OK (3 tests, 5 assertions)

スキップされたテストは、PHPUnit コマンドラインテストランナーの出力で S で示されます。

解説

tests/StackTest.php

このコードは、PHPのユニットテストにおいて、スタック(配列)の動作をテストするためのPHPUnitテストケースクラスの例です。PHPUnitのAttributes機能を使用して、テスト間の依存関係を指定しています。

まず、<?php declare(strict_types=1);は、スクリプトの厳格な型宣言を有効にするためのPHPの宣言です。

use PHPUnit\Framework\Attributes\Depends;は、PHPUnitフレームワークのAttributes機能を使用するためのインポートです。Attributesを使用することで、テストメソッド間の依存関係を宣言的に指定できます。

final class StackTest extends TestCaseは、TestCaseクラスを継承しているStackTestというテストケースクラスを定義しています。

テストケースクラス内には3つのテストメソッドがあります:

  1. public function testEmpty(): array: これはスタックが空であることを確認するテストメソッドです。$stack = [];で空の配列を作成し、$this->assertEmpty($stack);でスタックが空であることをアサートしています。テスト成功後に、この空のスタックを返しています。
  2. #[Depends('testEmpty')] public function testPush(array $stack): array: これはスタックに要素を追加するテストメソッドです。テストメソッドの引数としてtestEmptyメソッドから返された空のスタックを受け取ります。そして、$stack[] = 'foo';でスタックに要素を追加し、追加された要素が正しくスタックの末尾にあることをアサートしています。さらに、$this->assertNotEmpty($stack);でスタックが空でないことを確認しています。テスト成功後に、要素が追加されたスタックを返しています。
  3. #[Depends('testPush')] public function testPop(array $stack): void: これはスタックから要素を取り出すテストメソッドです。テストメソッドの引数としてtestPushメソッドから返された要素が追加されたスタックを受け取ります。そして、$this->assertSame('foo', array_pop($stack));でスタックの末尾から要素を取り出し、それが期待通りの要素(’foo’)であることをアサートしています。さらに、$this->assertEmpty($stack);でスタックが空であることを確認しています。このテストでは、testPushメソッドが成功していない場合はスキップされます。

ここで重要なのは、#[Depends('methodName')]属性を使用して、テストメソッド間の依存関係を定義していることです。依存関係を持つテストは、依存しているテストメソッドが成功した後にのみ実行されます。例えば、testPopメソッドはtestPushメソッドに依存していますので、testPushメソッドが成功しない場合、testPopメソッドはスキップされます。これにより、テストの実行順序を制御し、テスト間の依存関係を明確にすることができます。

依存関係にあるテストが失敗した場合

不具合を特定するには、失敗したテストに注目する必要があります。そのため、PHPUnit では依存関係にあるテストが失敗した際に そのテストの実行をスキップするようにしています。これにより、下記に示すようにテスト間の依存関係を利用して不具合の切り分けを行うことができます。

<?php declare(strict_types=1);
use PHPUnit\Framework\Attributes\Depends;
use PHPUnit\Framework\TestCase;

final class DependencyFailureTest extends TestCase
{
    public function testOne(): void
    {
        $this->assertTrue(false);
    }

    #[Depends('testOne')]
    public function testTwo(): void
    {
    }
}

上記のテストを実行すると、以下のような出力が得られる。

% docker compose run -it --rm php-cmd ./vendor/bin/phpunit tests/DependencyFailureTest.php 
[+] Building 0.0s (0/0)                                                                                                                            
[+] Building 0.0s (0/0)                                                                                                                            
PHPUnit 10.2.6 by Sebastian Bergmann and contributors.

Runtime:       PHP 8.1.12-1ubuntu4.2

FS                                                                  2 / 2 (100%)

Time: 00:00.076, Memory: 8.00 MB

There was 1 failure:

1) DependencyFailureTest::testOne
Failed asserting that false is true.

/work/tests/DependencyFailureTest.php:9

FAILURES!
Tests: 2, Assertions: 1, Failures: 1, Skipped: 1.

複数の依存関係をもつテストケース

テストは、複数のテスト依存属性を持つことができます。

デフォルトでは、PHPUnit はテストの実行順を変更しないので、 テストを実行する前にテストの依存関係を満たすことができるかどうかを確認する必要があります。

複数のテスト依存属性を持つテストは、最初の引数として最初のプロデューサのフィクスチャを受け取り、 2 番目の引数として 2 番目のプロデューサのフィクスチャを受け取ることになります。

PHPUnitでは、複数の依存関係をもつテストケースを書く場合、#[Depends('methodName1')]属性を使用します。これにより、テストメソッドが複数の別のテストメソッドに依存していることを宣言的に示すことができます。

以下は、複数の依存関係を持つテストケースの例です。

<?php declare(strict_types=1);
use PHPUnit\Framework\Attributes\Depends;
use PHPUnit\Framework\TestCase;

final class MultipleDependenciesTest extends TestCase
{
    public function testA(): string
    {
        $value = 'A';
        $this->assertEquals('A', $value);

        return $value;
    }

    public function testB(): string
    {
        $value = 'B';
        $this->assertEquals('B', $value);

        return $value;
    }

    #[Depends('testA')]
    #[Depends('testB')]
    public function testC(string $valueA, string $valueB): void
    {
        $this->assertEquals('A', $valueA);
        $this->assertEquals('B', $valueB);
    }
}

この例では、MultipleDependenciesTestクラスに3つのテストメソッドがあります:

  1. testA(): 値 ‘A’ が期待通りであるかをテストします。
  2. testB(): 値 ‘B’ が期待通りであるかをテストします。

そして、3番目のtestC(string $valueA, string $valueB)メソッドは、testA()testB()の両方の結果を引数として受け取ります。このメソッドには、#[Depends('testA')] #[Depends('testB')]属性が付いており、testA()testB()に依存していることを示しています。

テストの実行時、PHPUnitはtestC()メソッドの実行前にtestA()testB()を実行し、その結果をtestC()に渡します。そして、testC()メソッド内でそれらの結果を利用してテストを実行します。

上記のテストを実行すると、以下のような出力が得られる。

% docker compose run -it --rm php-cmd ./vendor/bin/phpunit tests/MultipleDependenciesTest.php
[+] Building 0.0s (0/0)                                                                                                                            
[+] Building 0.0s (0/0)                                                                                                                            
PHPUnit 10.2.6 by Sebastian Bergmann and contributors.

Runtime:       PHP 8.1.12-1ubuntu4.2

...                                                                 3 / 3 (100%)

Time: 00:00.046, Memory: 6.00 MB

OK (3 tests, 4 assertions)

フィクスチャとは

フィクスチャ(Fixture)は、ソフトウェアテストにおいて、テストデータやテスト環境を定義し、テストケースを実行する際に使用される準備済みの状態を指します。テストの実行に必要な初期データや環境を準備することで、テストの安定性や再現性を高めるのに役立ちます。

フィクスチャは以下のような目的で使用されます:

  1. 初期データの準備: テストケースが実行される前に、テスト対象のシステムやデータベースに初期データをセットアップすることがあります。これにより、テストが確実に同じ状態から始まることが保証されます。
  2. テストデータの提供: テストケースがテストデータを必要とする場合、フィクスチャはそのデータを提供します。これにより、テストコードはテストデータにアクセスしやすくなります。
  3. テスト環境のセットアップ: テストケースが特定の環境設定や依存関係を必要とする場合、フィクスチャはその環境をセットアップします。例えば、データベース接続や外部APIとの連携をテストする場合、適切な設定やモックをフィクスチャとして提供できます。

フィクスチャはテスト実行前後に実行されることが一般的です。一部のテストフレームワークでは、setUp()メソッドやtearDown()メソッドを使用してフィクスチャをセットアップおよびクリーンアップすることができます。

例えば、PHPUnitの場合、フィクスチャをセットアップするためにはsetUp()メソッドを使用し、クリーンアップのためにはtearDown()メソッドを使用します。これらのメソッドはテストケースの各メソッドの実行前と実行後に自動的に呼び出されます。

おわりに

今日は、PHPUnitでテストの依存関係を設定する方法についてご紹介しました。

本記事でご紹介したソースは、下記のリポジトリにあります。

https://github.com/Gate-Yossi/Ran/releases/tag/v2023.08.12
よっしー
よっしー

何か質問や相談があれば、コメントをお願いします。また、エンジニア案件の相談にも随時対応していますので、お気軽にお問い合わせください。

それでは、また明日お会いしましょう(^^)

コメント

タイトルとURLをコピーしました