こんにちは。よっしーです(^^)
今日は、PHPUnitにおけるフィクスチャについてご紹介します。
背景
テストは通常、”Arrange, Act, Assert “の構造に従います。
テストは通常、「準備、実行、検証」の構造に従います。つまり、必要な前提条件と入力(いわゆるテストフィクスチャ)を整え、テスト対象のオブジェクトに対して操作を行い、予期される結果が発生したことを確認することです。
これとは別に、「Arrange, Expect, Act」の構造もあります。これは、通常、アクションが例外を発生させることを期待する場合や、モックオブジェクトを使用して協力するオブジェクト間の通信を検証する場合に使用されます。具体的には、準備、期待、実行のステップに従ってテストを行います。
ときには、テストフィクスチャは単一のオブジェクトで構成されることもありますし、複雑なオブジェクトグラフであることもあります。そのため、設定に必要なコードの量も相応に増えていきます。テストフィクスチャのセットアップ作業の中で、実際のテストの内容が埋もれてしまうことがあります。特に、似たようなテストフィクスチャを持つ複数のテストを書く場合、この問題はさらに深刻になります。
PHPUnitは、テスト間でセットアップコードを再利用することをサポートしています。テストメソッドが実行される前に、setUp() というテンプレートメソッドが呼び出されます。ここでテストフィクスチャを作成することができます。テストメソッドの実行が終了したら(成功した場合でも失敗した場合でも)、tearDown() という別のテンプレートメソッドが呼び出されます。ここで、テスト対象のオブジェクトをクリーンアップすることができます。
例えば下記のテストケースがあるとします。
<?php declare(strict_types=1);
namespace example;
use PHPUnit\Framework\TestCase;
final class ExampleTest extends TestCase
{
private ?Example $example;
public function testSomething(): void
{
$this->assertSame(
'the-result',
$this->example->doSomething()
);
}
protected function setUp(): void
{
$this->example = new Example(
$this->createStub(Collaborator::class)
);
}
protected function tearDown(): void
{
$this->example = null;
}
}
setUp() および tearDown() テンプレートメソッドは、テストケースクラスの各テストメソッド (および新しいインスタンス) に対して一度だけ実行されます。
setUp() および tearDown() テンプレートメソッドの問題点のひとつは、 これらのメソッドが管理するテストフィクスチャ (上の例では $this->example プロパティ) を使用していないテストでもコールされてしまうということです。
もうひとつの問題は、継承の際に発生します。
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
abstract class MyTestCase extends TestCase
{
protected function setUp(): void
{
// ...
}
}
<?php declare(strict_types=1);
namespace example;
use PHPUnit\Framework\TestCase;
final class ExampleTest extends MyTestCase
{
protected function setUp(): void
{
// ...
}
}
もしExampleTest::setUp()を実装する際にparent::setUp()を呼び出し忘れると、MyTestCaseが提供する機能が動作しなくなります。このリスクを軽減するために、PHPUnit\Framework\Attributes\BeforeおよびPHPUnit\Framework\Attributes\Afterという属性が利用可能です。これらを使用することで、複数のメソッドをそれぞれテストの前後に呼び出すように設定できます。
tearDown()よりもsetUp()の方が多く使用される
setUp()とtearDown()は理論的にはうまく対称的ですが、実際にはそうではありません。実際には、setUp()でファイルやソケットなどの外部リソースを割り当てた場合にのみ、tearDown()を実装する必要があります。setUp()で大きなオブジェクトグラフを作成し、テストオブジェクトのプロパティに格納しない限り、通常はtearDown()を無視して構いません。
ただし、setUp()で大きなオブジェクトグラフを作成し、それらをテストオブジェクトのプロパティに格納する場合、tearDown()でそれらのオブジェクトを保持する変数をunset()して、ガベージコレクションが早く行われるようにすることがあります。
setUp()(またはテストメソッド)で作成された、テストオブジェクトのプロパティに格納されているオブジェクトは、PHPUnitを実行するPHPプロセスの終了時にのみ自動的にガベージコレクションされます。
シェアリング・フィクスチャー
テスト間でフィクスチャを共有する理由はほとんどありませんが、ほとんどの場合、テスト間でフィクスチャを共有する必要性は未解決の設計上の問題に起因しています。
複数のテスト間で共有するのが理にかなっているフィクスチャの良い例は、データベース接続です。データベースに一度ログインし、各テストごとに新しい接続を作成するのではなく、再利用することで、テストをより高速に実行できます。
setUpBeforeClass()とtearDownAfterClass()というテンプレートメソッドは、テストケースクラスの最初のテストが実行される前と、最後のテストが実行された後にそれぞれ呼び出されます。
下記の例では、setUpBeforeClass()とtearDownAfterClass()テンプレートメソッドを使用して、テストケースクラスの最初のテストの前にデータベースに接続し、最後のテストの後にデータベースから切断するようにしています。
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class DatabaseTest extends TestCase
{
private static $dbh;
public static function setUpBeforeClass(): void
{
self::$dbh = new PDO('sqlite::memory:');
}
public static function tearDownAfterClass(): void
{
self::$dbh = null;
}
}
フィクスチャをテスト間で共有することが、テストの価値を減少させることは強調するに足りません。根本的な設計の問題は、オブジェクトが疎結合になっていないことです。テストダブル(スタブなど)を使用して根本的な設計の問題を解決し、テスト間で実行時の依存関係を作成してデザインの改善の機会を無視するよりも、より良い結果が得られます。
Global State
シングルトンを使用するコードやグローバル変数を使用するコードをテストするのは難しいです。通常、テストしたいコードはグローバル変数と強く結合しており、その作成を制御することができません。また、1つのテストがグローバル変数を変更すると、別のテストが壊れる可能性もあります。
PHPでは、グローバル変数は以下のように動作します:
グローバル変数$foo = ‘bar’; は、$GLOBALS[‘foo’] = ‘bar’; として保存されます。
$GLOBAL変数は、いわゆるスーパーグローバル変数です。
スーパーグローバル変数は組み込み変数であり、すべてのスコープで常に使用可能です。
関数またはメソッドのスコープ内では、$fooに直接アクセスするか、global $foo; を使用してグローバル変数への参照を持つローカル変数を作成することで、グローバル変数$fooにアクセスできます。
グローバル変数に加えて、クラスの静的属性もグローバル状態の一部です。
PHPUnitは、グローバルおよびスーパーグローバル変数($GLOBALS、$_ENV、$_POST、$_GET、$_COOKIE、$_SERVER、$_FILES、$_REQUEST)への変更が他のテストに影響を与えないようにテストを実行するオプションがあります。–globals-backupオプションを使用するか、XML設定ファイルでbackupGlobals=”true”を設定することで、この動作を有効にできます。
–static-backupオプションを使用するか、XML設定ファイルでbackupStaticAttributes=”true”を設定することで、この隔離をクラスの静的プロパティにも拡張できます。
PHPUnit\Framework\Attributes\BackupGlobals属性は、グローバル変数のバックアップとリストア操作を制御するために使用できます。
PHPUnit\Framework\Attributes\ExcludeGlobalVariableFromBackup属性は、特定のグローバル変数をグローバル変数のバックアップとリストア操作から除外するために使用できます。
PHPUnit\Framework\Attributes\BackupStaticProperties属性は、クラスの静的プロパティのバックアップとリストア操作を制御するために使用できます。これは、各テストの前にすべての宣言されたクラスのすべての静的プロパティに影響を与え、その後リストアします。テストが開始される時点で宣言されているすべてのクラスが処理されます。テストクラス自体だけでなく、その他のクラスも対象に含まれます。ただし、これは静的関数内の静的変数には適用されず、静的クラスのプロパティにのみ適用されます。
PHPUnit\Framework\Attributes\ExcludeStaticPropertyFromBackup属性は、特定の静的プロパティを静的プロパティのバックアップとリストア操作から除外するために使用できます。
これらの属性を活用することで、グローバル変数と静的プロパティに関連するPHPUnitの動作をより細かく制御し、テスト実行中の隔離と一貫性を向上させることができます。
ユニットテストでは、テスト対象の静的プロパティの値を setUp() コードで明示的にリセットすることを推奨します (理想的には、その後に実行されるテストに影響を与えないように tearDown() も行います)。
おわりに
PHPUnitにおけるフィクスチャについてご紹介しました。
何か質問や相談があれば、コメントをお願いします。また、エンジニア案件の相談にも随時対応していますので、お気軽にお問い合わせください。
それでは、また明日お会いしましょう(^^)
コメント