まえがき
今所属しているプロジェクトでFWとPHPのバージョンアップを担当することになり、
そのついでに phpunit をバージョンアップしたのですが、詰まったところがググっても中々出てこなかったので、
自分用のメモ含め誰かの知見になれば良いかなと思い、記事にしました。
前提
Laravel6 -> Laravel10 にアップデートに併せて phpunit8 -> phpunit10 にアップデートした際の
ケースでかつ僕のいたプロジェクトで対応した内容になります。
同じ様に実施していても、別のエラーなど出る可能性があるので参考程度に読んでいただければと思います。
composer.json をアップデートする
今回吾輩のプロジェクトではLaravel10にバージョンアップするので
GitHubのタグブランチに倣って composer.json の指定バージョンを ^10.0
としておきます
"phpunit/phpunit": "^10.0"
テスト実行&エラー箇所を修正していく
phpunit.xmlの仕様変更に対応する
早速テストコードを実行すると、なんか出てきました・・・
"--migrate-configuration"
でマイグレーションしろと言われているみたいです。
root@dbe6820a2d5c:/var/www# ./vendor/bin/phpunit tests/SampleSeriviceTest.php
PHPUnit 10.0.16 by Sebastian Bergmann and contributors.
・・・・・
There was 1 PHPUnit warning:
1) Your XML configuration validates against a deprecated schema. Migrate your XML configuration using "--migrate-configuration"!
WARNINGS!
Tests: 4, Assertions: 63, Warnings: 1.
なので、言われた通りに実行すると、
バックアップファイルとマイグレーション後のファイルの合計2ファイルが生成されました!
root@dbe6820a2d5c:/var/www# phpunit --migrate-configuration
PHPUnit 10.0.16 by Sebastian Bergmann and contributors.
Created backup: /var/www/phpunit.xml.bak
Migrated configuration: /var/www/phpunit.xml
ファイルがバージョンに対応したものになったっぽいので、もう一度叩いてみます。すると・・・
どうやら今度は phpunit.xml の中にある extension
タグは boostrap
じゃないんでしょうか?
的な事を言われている様です。なので直しに行きます。
root@dbe6820a2d5c:/var/www# phpunit tests/Services/UserPurchasedBookSet/BookSetAssignmentServiceTest.php
・・・ 中略 ・・・
1) Test results may not be as expected because the XML configuration file did not pass validation:
Line 35:
- Element 'extension': This element is not expected. Expected is ( bootstrap ).
こんな感じに直してみましょうか。
<extensions>
- <extension class="Tests\Extension"/>
+ <bootstrap class="Tests\Extension"/>
</extensions>
これで再度テストを実行すると・・・また違うエラーが・・・!(やめてクレェぇぇぇぇ)
Message: Interface "PHPUnit\Runner\BeforeFirstTestHook" not found
Location: /var/www/tests/Extension.php:8
Hook を使った形から Subscriber を使った形に変更する
ここからがこの記事の本題になります。
phpunit9 までは処理の前後やエラー時などに行いたい処理を xxxxTestHook を implements したクラスに記述することで実行していました。
ところが、phpunit10 では「単一責任の原則」にこの実装が反するとのことで大幅な改修が加えられており、これらのファイルが丸々削除されてしまいました。
その替わりに「Subscriber」という機構が用意され、そちらを使うようになっています。(参考:作者のお言葉)
僕のいるプロジェクトではこれらの処理を上記にある Tests\Extension.php に必要な xxxxTestHook を implements していたため動かなくなってしまっていました。なので、今回はこのファイルが動くように関連ファイルを直していきます。
修正1:対応するイベントを各Subscriberへ移設(分割)する
元々うちのプロジェクトでは phpunit.xml から呼び出されていた Extension.php は以下の様になっていました
見てもらうとわかる様に各ライフサイクルに対応した xxxHook が implements されており対応するメソッドに具体的な処理が実装されています。
現在はテスト実行時の一番最初と一番最後に一回ずつ指定した処理を走らせているので同じ動きになるよう実装していきます。
<?php
namespace Tests;
use PHPUnit\Runner\AfterLastTestHook;
use PHPUnit\Runner\BeforeFirstTestHook;
class Extension implements BeforeFirstTestHook, AfterLastTestHook
{
public function executeBeforeFirstTest(): void
{
// 一番最初だけ色々ゴニョっている処理が書かれている
}
public function executeAfterLastTest(): void
{
// 一番最後だけ色々ゴニョっている処理が書かれている
}
}
BeforeFirstTestHook@executeBeforeFirstTestに対応するのは「StartedSubscriber」になるので、それを implements したクラスを StartedSubscriberImpl.php
とかそれっぽそうな名前で作成して executeBeforeFirstTest
の中身を移します。
<?php
namespace Tests;
use PHPUnit\Event\TestRunner\Started;
use PHPUnit\Event\TestRunner\StartedSubscriber;
class StartedSubscriberImpl implements StartedSubscriber
{
public function notify(Started $event): void
{
// 一番最初だけ色々ゴニョっている処理が書かれている
}
}
続いて、
AfterLastTestHook@executeAfterLastTestに対応するのは「FinishedSubscriber」になるので、それを implements したクラスを FinishedSubscriberImpl.php
とかそれっぽそうな名前で作成して executeAfterLastTest
の中身を移します。
<?php
namespace Tests\Testing\Events\TestRunner;
use PHPUnit\Event\TestRunner\Finished;
use PHPUnit\Event\TestRunner\FinishedSubscriber;
class FinishedSubscriberImpl implements FinishedSubscriber
{
public function notify(Finished $event): void
{
// 一番最後だけ色々ゴニョっている処理が書かれている
}
}
修正2:実装したSubscriberの呼び出し
ファイルを分割したら、これらの処理をイベントとして登録してやる必要があります。
それには先ほどから何度も名前が上がっていた独自クラス「Extension.php」内で Subscriber を登録していく必要があります。
ということで、修正後のファイルがこちら!すごいシンプルになりましたね。
<?php
namespace Tests;
use PHPUnit\Runner\Extension\Extension as RunnerExtension;
use PHPUnit\Runner\Extension\Facade;
use PHPUnit\Runner\Extension\ParameterCollection;
use PHPUnit\TextUI\Configuration\Configuration;
use Tests\FinishedSubscriberImpl;
use Tests\StartedSubscriberImpl;
final class Extension implements RunnerExtension
{
public function bootstrap(Configuration $configuration, Facade $facade, ParameterCollection $parameters): void
{
$facade->registerSubscribers(
new StartedSubscriberImpl(),
new FinishedSubscriberImpl()
);
}
}
phpunit10 では phpunit.xml の extensions タグに設定したファイルは Extension の Interface を必ず実装する必要があります。
なので、きっちり implements に定義しておきます。
use PHPUnit\Runner\Extension\Extension as RunnerExtension;
final class Extension implements RunnerExtension
続いて Extension の Interface は boostrap
メソッドを持っているのでこちらを実装します。
そうすると、引数が3つあります。このうちの Facade クラスに registerSubscribers
というイベントを登録するメソッドが生えており、ここから必要な Subscriber を登録することができます。
ミソは呼び出し先でスプレッド構文を使用しているので複数渡すときもこちらは気にせずカンマ区切りで指定すれば良いです。地味に便利ですね!
// ↓ ヨォ!これだよこれ!ここの行にある Facade ってあるだろ?これを使うんだよ!!
public function bootstrap(Configuration $configuration, Facade $facade, ParameterCollection $parameters): void
{
// ↓ そんで、$facade には registerSubscribers ってあるからそれ使うんだ。まじで便利だよな!!!
$facade->registerSubscribers(
new StartedSubscriberImpl(),
new FinishedSubscriberImpl()
);
}
こうして、疎通を確認するために dump を仕込むと見事、処理が意図した通りに最初と最後だけ実行されていることが確認できました!
めでたしめでたし
最後まで読んでいただきありがとうございました。