こんにちは。よっしーです(^^)
今日は、PHPUnitにおけるテスト機能の拡張についてご紹介します。
背景
具体的なテストケースの強化
具体的なテストケースを機能を追加することで、PHPUnitを拡張することができます。
たとえば、具体的なテストケースでシステムの生成した値が正規表現と一致することをアサートするために、アサーションを使用することができます。
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class OrderIdGeneratorTest extends TestCase
{
public function testGenerateGeneratesId(): void
{
$orderIdGenerator = new OrderIdGenerator;
$orderId = $orderIdGenerator->generate();
$this->assertMatchesRegularExpression(
'/^[a-f0-9]{8}-[a-f0-9]{4}$/',
sprintf(
'Failed asserting that "%s" is a valid order ID.',
$orderId,
),
);
}
}
この具体的なテストケースをドメイン固有のアサーションを抽出して拡張することができます。
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class OrderIdGeneratorWithDomainSpecificAssertionTest extends TestCase
{
public function testGenerateGeneratesId(): void
{
$orderIdGenerator = new OrderIdGenerator;
$orderId = $orderIdGenerator->generate();
$this->assertStringIsOrderId($orderId);
}
private function assertStringIsOrderId(string $value): void
{
$this->assertMatchesRegularExpression(
'/^[a-f0-9]{8}-[a-f0-9]{4}$/',
$value,
sprintf(
'Failed asserting that "%s" is a valid order ID.',
$value,
),
);
}
}
このように、テストケースを拡張することで、よりドメイン固有のアサーションを作成し、テストコードをより読みやすく、保守性の高いものにすることができます。
抽象的なテストケースの抽出
PHPUnitを拡張するために、AbstractTestCaseを抽出して継承を介して他の具体的なテストケースと機能を共有することができます。
たとえば、上記のドメイン固有のアサーションをAbstractTestCaseに取り込みたいかもしれません。
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
abstract class AbstractTestCase extends TestCase
{
final protected function assertStringIsOrderId(string $value): void
{
$this->assertMatchesRegularExpression(
'/^[a-f0-9]{8}-[a-f0-9]{4}$/',
$value,
sprintf(
'Failed asserting that "%s" is a valid order ID.',
$value,
),
);
}
}
そして、具体的なテストケースを拡張することで、AbstractTestCaseを利用することができます。
<?php declare(strict_types=1);
final class OrderIdGeneratorExtendingAbstractTestCaseTest extends AbstractTestCase
{
public function testGenerateGeneratesId(): void
{
$orderIdGenerator = new OrderIdGenerator;
$orderId = $orderIdGenerator->generate();
$this->assertStringIsOrderId($orderId);
}
}
これにより、抽象テストケースを具体的なテストケースで拡張することで、共通の機能を簡単に再利用できるようになります。ドメイン固有のアサーションなどの機能を抽象テストケースにまとめることで、テストコードの再利用性と保守性が向上します。
トレイトの抽出
PHPUnitを拡張するために、トレイトを抽出して継承を介して具体的なテストケースと機能を共有することができます。
たとえば、先ほどのドメイン固有のアサーションをトレイトに取り込みたいかもしれません。
<?php declare(strict_types=1);
trait AssertionTrait
{
final protected function assertStringIsOrderId(string $value): void
{
$this->assertMatchesRegularExpression(
'/^[a-f0-9]{8}-[a-f0-9]{4}$/',
$value,
sprintf(
'Failed asserting that "%s" is a valid order ID.',
$value,
),
);
}
}
具体的なテストケースでトレイトを使用することで、トレイト内の機能を簡単に再利用できるようになります。
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class OrderIdGeneratorUsingAssertionTraitTest extends TestCase
{
use AssertionTrait;
public function testGenerateGeneratesId(): void
{
$orderIdGenerator = new OrderIdGenerator;
$orderId = $orderIdGenerator->generate();
$this->assertStringIsOrderId($orderId);
}
}
これにより、トレイトを具体的なテストケースで使用することで、共通の機能を簡単に取り込むことができます。ドメイン固有のアサーションなどの機能をトレイトにまとめることで、テストコードの再利用性と保守性が向上します。
テストランナーの拡張
PHPUnit を拡張するには、拡張モジュールを実装して登録します。
拡張機能の実装
PHPUnitの拡張機能は、PHPUnit\Runner\Extension\Extensionインターフェースを実装するクラスです。
この拡張インターフェースは、PHPUnitの設定、拡張ファサード、および拡張パラメータコレクションを受け取るbootstrap()メソッドを宣言しています。
<?php declare(strict_types=1);
namespace Vendor\ExampleExtensionForPhpunit;
use PHPUnit\Runner\Extension\Extension;
use PHPUnit\Runner\Extension\Facade;
use PHPUnit\Runner\Extension\ParameterCollection;
use PHPUnit\TextUI\Configuration\Configuration;
final class ExampleExtension implements Extension
{
public function bootstrap(
Configuration $configuration,
Facade $facade,
ParameterCollection $parameters
): void {
if ($configuration->noOutput()) {
return;
}
$message = 'the-default-message';
if ($parameters->has('message')) {
$message = $parameters->get('message');
}
$facade->registerSubscriber(new ExampleSubscriber($message));
$facade->registerTracer(new ExampleTracer);
}
}
PHPUnitの設定はPHPUnit\TextUI\Configuration\Configurationのインスタンスであり、デフォルトの設定、XML構成ファイル、およびコマンドラインオプションから設定オプションをマージした後のPHPUnitの設定にアクセスできます。
設定オブジェクトを検査して、拡張機能の動作を調整することができます。たとえば、コンソールに出力を表示する拡張機能をPHPUnitに追加したい場合、PHPUnitのユーザーが色を使用するか、モノクロの出力を希望するかを知る必要があるかもしれません。
拡張ファサードはPHPUnit\Runner\Extension\Facadeのインスタンスであり、イベントサブスクライバとイベントトレーサを登録することができます。
パラメータコレクションはPHPUnit\Runner\Extension\ParameterCollectionのインスタンスであり、PHPUnitのXML構成ファイルを介してユーザーが提供した拡張パラメータにアクセスすることができます。
パラメータコレクションを使用して、拡張機能の動作をユーザーが設定できるようにすることができます。
イベントサブスクライバの実装
イベントサブスクライバは、イベントサブスクライバインターフェースを実装するクラスです。
イベントサブスクライバインターフェースは、対応するイベントクラスのインスタンスを受け取る単一のnotify()メソッドを宣言します。
<?php declare(strict_types=1);
namespace Vendor\ExampleExtensionForPhpunit;
use PHPUnit\Event\TestRunner\ExecutionFinished;
use PHPUnit\Event\TestRunner\ExecutionFinishedSubscriber;
final class ExampleSubscriber implements ExecutionFinishedSubscriber
{
public function __construct(private readonly string $message)
{
}
public function notify(ExecutionFinished $event): void
{
print __METHOD__ . PHP_EOL . $this->message . PHP_EOL;
}
}
イベントサブスクライバを拡張ファサードに登録した後、PHPUnitは対応するイベントクラスのイベントを発行する際に、イベントサブスクライバに通知します。
イベントトレーサの実装
イベントトレーサは、PHPUnit\Event\Tracer\Tracerインターフェースを実装するクラスです。
トレーサインターフェースは、単一のtrace()メソッドを宣言し、イベントを受け取ります。
<?php declare(strict_types=1);
namespace Vendor\ExampleExtensionForPhpunit;
use PHPUnit\Event\Event;
use PHPUnit\Event\Tracer\Tracer;
final class ExampleTracer implements Tracer
{
public function trace(Event $event): void
{
// ...
}
}
イベントトレーサを拡張ファサードに登録した後、PHPUnitはすべてのイベントをトレーサに通知します。
イベントの理解
イベントはPHPUnit\Event\Eventインターフェースを実装するクラスです。
PHPUnit\Event\Eventインターフェースは、テレメトリ情報にアクセスできるtelemetryInfo()メソッドと、イベントの文字列表現を返すasString()メソッドを宣言しています。
各イベントは、PHPUnitがイベントを登録および発行する際に使用可能な情報にアクセスするための追加のメソッドを実装する場合があります。
これらのイベントは、イベントサブスクライバやイベントトレーサで消費し、検査、処理することができます。
PHPUnitが現在発行するすべてのイベントのリストは、付録に記載されています。
拡張モジュールの共有
PHPUnit 拡張モジュールは、PHAR あるいは Composer パッケージとして共有することができま
拡張モジュールを PHAR として共有
拡張モジュールのユーザが PHPUnit を PHAR としてインストールしたがる場合は、 拡張モジュールも PHAR として公開するのが一番です。
拡張モジュールを PHAR としてロードできるようにするには、 PHAR マニフェスト <https://github.com/phar-io/manifest> を含める必要があります。
<?xml version="1.0" encoding="utf-8" ?>
<phar xmlns="https://phar.io/xml/manifest/1.0">
<contains name="phpunit/phpunit-test-extension" version="1.0.0" type="extension">
<extension for="phpunit/phpunit" compatible="^10.0"/>
</contains>
<requires>
<php version="^8.1"/>
</requires>
<copyright>
<author name="Sebastian Bergmann" email="sebastian@phpunit.de"/>
<license type="BSD-3-Clause" url="https://github.com/sebastianbergmann/phpunit/blob/master/LICENSE"/>
</copyright>
</phar>
拡張モジュールを Composer パッケージとして共有
拡張モジュールのユーザが PHPUnit を Composer パッケージとしてインストールしたい場合は、 Composer パッケージを作成するのが一番です。
拡張モジュールの登録
ひとつあるいは複数の PHPUnit 拡張モジュールを PHAR や Composer パッケージから登録するには、 PHPUnit XML 設定ファイルの extensions、bootstrap および parameters 要素を使用します。
PHAR からの拡張モジュールの登録
PHPUnit を PHAR としてインストールする場合は、PHAR から拡張モジュールを読み込むのがベストです。
phpunit 要素の extensionsDirectory 属性を使用すると、 PHPUnit が PHAR から拡張モジュールを読み込む際のディレクトリを指定することができます。
<?xml version="1.0" encoding="UTF-8"?>
<phpunit
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.0/phpunit.xsd"
extensionsDirectory="../phpunit-extensions/"
>
<!-- ... -->
<extensions>
<bootstrap class="Vendor\ExampleExtensionForPhpunit\ExampleExtension">
<parameter name="message" value="the-message"/>
</bootstrap>
</extensions>
<!-- ... -->
</phpunit>
Composer パッケージからの拡張モジュールの登録
PHPUnit を Composer パッケージとしてインストールする場合は、 Composer パッケージから拡張モジュールを読み込むのがベストです。
extensionsDirectory 属性を設定する必要はありません。 Composer パッケージから読み込んだ拡張モジュールは、 Composer のオートロード機能によって利用できるようになります。
<?xml version="1.0" encoding="UTF-8"?>
<phpunit
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.0/phpunit.xsd"
>
<!-- ... -->
<extensions>
<bootstrap class="Vendor\ExampleExtensionForPhpunit\ExampleExtension">
<parameter name="message" value="the-message"/>
</bootstrap>
</extensions>
<!-- ... -->
</phpunit>
PHPUnitのデバッグ
テストランナーの –log-events-text CLI オプションを使用すると、 各イベントをプレーンテキストでストリームに書き出すことができます。以下の例では、–no-output を使用してデフォルトの進捗出力とデフォルトの結果出力を無効にしています。そして –log-events-text php://stdout を使用して、イベント情報を標準出力に書き出します:
phpunit --no-output --log-events-text php://stdout
PHPUnit Started (PHPUnit 10.0.0 using PHP 8.2.1 (cli) on Linux)
Test Runner Configured
Test Suite Loaded (2 tests)
Event Facade Sealed
Test Runner Started
Test Suite Sorted
Test Runner Execution Started (2 tests)
Test Suite Started (ExampleTest, 2 tests)
Test Preparation Started (ExampleTest::testOne)
Test Prepared (ExampleTest::testOne)
Assertion Succeeded (Constraint: is true)
Test Passed (ExampleTest::testOne)
Test Finished (ExampleTest::testOne)
Test Preparation Started (ExampleTest::testTwo)
Test Prepared (ExampleTest::testTwo)
Assertion Failed (Constraint: is identical to 'foo', Value: 'bar')
Test Failed (ExampleTest::testTwo)
Failed asserting that two strings are identical.
Test Finished (ExampleTest::testTwo)
Test Suite Finished (ExampleTest, 2 tests)
Test Runner Execution Finished
Test Runner Finished
PHPUnit Finished (Shell Exit Code: 1)
あるいは、-log-events-verbose-text CLI オプションを使用して、リソースの消費に関する情報(テストランナーが開始されてからの時間、前のイベントからの時間、メモリ使用量)を含めることもできます。
phpunit --no-output --log-events-verbose-text php://stdout
[00:00:00.000046482 / 00:00:00.000006987] [4194304 bytes] PHPUnit Started (PHPUnit 10.0.0 using PHP 8.2.1 (cli) on Linux)
[00:00:00.048195557 / 00:00:00.048149075] [4194304 bytes] Test Runner Configured
[00:00:00.067646038 / 00:00:00.019450481] [6291456 bytes] Test Suite Loaded (2 tests)
[00:00:00.075942220 / 00:00:00.008296182] [6291456 bytes] Event Facade Sealed
[00:00:00.076452360 / 00:00:00.000510140] [6291456 bytes] Test Runner Started
[00:00:00.084421682 / 00:00:00.007969322] [6291456 bytes] Test Suite Sorted
[00:00:00.084664485 / 00:00:00.000242803] [6291456 bytes] Test Runner Execution Started (2 tests)
[00:00:00.085240320 / 00:00:00.000575835] [6291456 bytes] Test Suite Started (ExampleTest, 2 tests)
[00:00:00.086992385 / 00:00:00.001752065] [6291456 bytes] Test Preparation Started (ExampleTest::testOne)
[00:00:00.087443560 / 00:00:00.000451175] [6291456 bytes] Test Prepared (ExampleTest::testOne)
[00:00:00.088237489 / 00:00:00.000793929] [6291456 bytes] Assertion Succeeded (Constraint: is true)
[00:00:00.089076305 / 00:00:00.000838816] [6291456 bytes] Test Passed (ExampleTest::testOne)
[00:00:00.091027624 / 00:00:00.001951319] [6291456 bytes] Test Finished (ExampleTest::testOne)
[00:00:00.091110095 / 00:00:00.000082471] [6291456 bytes] Test Preparation Started (ExampleTest::testTwo)
[00:00:00.091158739 / 00:00:00.000048644] [6291456 bytes] Test Prepared (ExampleTest::testTwo)
[00:00:00.091991799 / 00:00:00.000833060] [6291456 bytes] Assertion Failed (Constraint: is identical to 'foo', Value: 'bar')
[00:00:00.099242925 / 00:00:00.007251126] [8388608 bytes] Test Failed (ExampleTest::testTwo)
Failed asserting that two strings are identical.
[00:00:00.099386498 / 00:00:00.000143573] [8388608 bytes] Test Finished (ExampleTest::testTwo)
[00:00:00.099437634 / 00:00:00.000051136] [8388608 bytes] Test Suite Finished (ExampleTest, 2 tests)
[00:00:00.103014760 / 00:00:00.003577126] [8388608 bytes] Test Runner Execution Finished
[00:00:00.103207309 / 00:00:00.000192549] [8388608 bytes] Test Runner Finished
[00:00:00.105879902 / 00:00:00.002672593] [8388608 bytes] PHPUnit Finished (Shell Exit Code: 1)
テストランナーのラッピング
PHPUnit\TextUI\Applicationクラスは、PHPUnit独自のCLIテストランナーのエントリーポイントです。ParaTestなどを構築するためにPHPUnitをラップしたい開発者によって(再)使用されることは意図されていません。
テストの実際の実行には、PHPUnit\TextUI\ApplicationはPHPUnit\TextUI\TestRunner::run()を使用します。
PHPUnit\TextUI\TestRunner::run()は、PHPUnit\TextUI\Configuration\Configuration、PHPUnit\Runner\ResultCache\ResultCache、およびPHPUnit\Framework\TestSuiteを必要とします。
PHPUnit\TextUI\Configuration\Configurationは、PHPUnit\TextUI\Configuration\Builder::build()を使用して構築できます。このメソッドには$_SERVER[‘argv’]を渡す必要があります。このメソッドはCLIの引数/オプションを解析し、XML構成ファイルをロードします(ロードできる場合)。
PHPUnit\Framework\TestSuiteは、PHPUnit\TextUI\Configuration\ConfigurationからPHPUnit\TextUI\Configuration\TestSuiteBuilder::build()を使用して構築できます。
PHPUnit\TextUI\TestRunnerは@internalとマークされていますが、PHPUnitのテストランナーをラップしたい開発者によって(再)使用されることを意図しています。
おわりに
PHPUnitにおけるテスト機能の拡張についてご紹介しました。
何か質問や相談があれば、コメントをお願いします。また、エンジニア案件の相談にも随時対応していますので、お気軽にお問い合わせください。
それでは、また明日お会いしましょう(^^)
コメント