TestFinishedSubscriberクラスの使われ方
関連処理は大きく分けて以下の2つになります。
-
TestFinishedSubscriber
クラスをイベントとして登録する -
イベントの発火で
TestFinishedSubscriber
クラスが実行される
また、この後のクラス間の呼び出し処理は一部省略しているところがあることをご了承ください。
TestFinishedSubscriberクラスをイベントとして登録する
まずは「TestFinishedSubscriber
クラスをイベントとして登録する」部分から見ていきます。
// 2-1 // PHPUnit\TextUI\Application public function run(array $argv): int { // ...略... $printer = OutputFacade::init( $configuration, $extensionReplacesProgressOutput, $extensionReplacesResultOutput, ); // ...略... } // ...クラス間の呼び出し省略... // 2-2 // PHPUnit\TextUI\Output\Default\ProgressPrinter\ProgressPrinter private function registerSubscribers(Facade $facade): void { $facade->registerSubscribers( new BeforeTestClassMethodErroredSubscriber($this), new TestConsideredRiskySubscriber($this), new TestErroredSubscriber($this), new TestFailedSubscriber($this), new TestFinishedSubscriber($this), new TestMarkedIncompleteSubscriber($this), new TestPreparedSubscriber($this), new TestRunnerExecutionStartedSubscriber($this), new TestSkippedSubscriber($this), new TestTriggeredDeprecationSubscriber($this), new TestTriggeredNoticeSubscriber($this), new TestTriggeredPhpDeprecationSubscriber($this), new TestTriggeredPhpNoticeSubscriber($this), new TestTriggeredPhpunitDeprecationSubscriber($this), new TestTriggeredPhpunitWarningSubscriber($this), new TestTriggeredPhpWarningSubscriber($this), new TestTriggeredWarningSubscriber($this), ); } // ...クラス間の呼び出し省略... // 2-3 // PHPUnit\Event\DirectDispatcher public function registerSubscriber(Subscriber $subscriber): void { // ...略... $this->subscribers[$eventClassName][] = $subscriber; }
エントリポイントであるApplication
クラス(Application.php)のrun
メソッドをみていきます(2-1)。
OutputFacade
クラスのinit
メソッドを呼び出してる箇所があり、その処理の中でProgressPrinter
クラスのregisterSubscribers
メソッドを呼び出しています。
さらに中を辿っていくとFacade
クラスのregisterSubscribers
メソッドで複数のSubscriberを引数として渡している処理があります(2-2)。
この中にTestFinishedSubscriber
クラスがあり、また他のSubscriberたちも渡されているようです。
さらに辿ると、最終的にDirectDispatcher
のregisterSubscriber
メソッドに行き着きます(2-3)。
DirectDispatcher
クラスの$subscribers
プロパティにEvent名をキー、Subscriberをバリュー(配列)として格納しています。具体的には、キーはPHPUnit\Event\Test\Finished
という文字列、バリューはTestFinishedSubscriber
オブジェクトが配列として入っています。
大事なのは、この時点ではまだプロパティにデータを追加しただけで、実際の進捗結果の出力処理は動かないということです。
イベントの発火でTestFinishedSubscriberクラスが実行される
次は「イベントの発火でTestFinishedSubscriber
クラスが実行される」部分を見ていきましょう。
// 3-1 // PHPUnit\TextUI\Application public function run(array $argv): int { // ...略... $runner = new TestRunner; $runner->run( $configuration, $resultCache, $testSuite, ); // ...略... } // 3-2 // PHPUnit\Framework\TestRunner public function run(TestCase $test): void { // ...略... $test->runBare(); // ...略... if ($test->wasPrepared()) { Facade::emitter()->testFinished( $test->valueObjectForEvents(), $test->numberOfAssertionsPerformed(), ); } } // 3-3 // PHPUnit\Event\DispatchingEmitter public function testFinished(Code\Test $test, int $numberOfAssertionsPerformed): void { $this->dispatcher->dispatch( new Test\Finished( $this->telemetryInfo(), $test, $numberOfAssertionsPerformed, ), ); } // ...クラス間の呼び出し省略... // 3-4 // PHPUnit\Event\DirectDispatcher public function dispatch(Event $event): void { // ...略... foreach ($this->subscribers[$eventClassName] as $subscriber) { try { /** @phpstan-ignore method.notFound */ $subscriber->notify($event); } catch (Throwable $t) { $this->handleThrowable($t); } } }
再度、エントリポイントであるApplication
クラスのrun
メソッドに戻りましょう(3-1)。TestRunner
クラスのrun
メソッドを呼び出しています。
次にTestRunner
のrun
メソッドでの処理を見ていきます(3-2)。このメソッド全体の詳細は省略しますが、テスト実行の核心部分は$test->runBare();
にあり、ここでテストケースを実際に実行します。
しかし今回注目するのは、メソッドの最後の方にあるFacade::emitter()->testFinished
です(省略している部分にもFacade::emitter()
によるその他のメソッド呼び出しが散見されます)。
生成箇所を辿るとFacade::emitter()
はDispatchingEmitter
クラスということがわかります(3-3)。DispatchingEmitter
クラスのtestFinished
メソッドではDispatchingEmitter
クラスがPHPUnit\Event\Test\Finished
クラスのインスタンスを渡しています。
最終的に呼ばれるのはDirectDispatcher
クラスのdispatch
メソッドです(3-4)。3-3で渡されたPHPUnit\Event\Test\Finished
クラスのインスタンスに基づき、$subscribers
プロパティのPHPUnit\Event\Test\Finished
(文字列)キーに登録されている配列がforeachで回ります。
ここで、以前登録したTestFinishedSubscriber
クラスが対象となりnotify
メソッドが呼び出されるのです。
ここまでの流れを1つの図にしてみると、下記のようになります。
Observerパターン
ここまで固有名詞による説明をしていませんでしたが、実はこの一連の処理は「Observerパターン」というデザインパターンです。初見だとどのような流れかを理解するのは難しいですが、覚えてしまうとさまざまな場面で利用されていることに気づく有用なデザインパターンです。
Observerパターンとは「あるオブジェクトの状態が変化した際、それに依存するすべてのオブジェクトに対して自動的に通知、更新を行う」ものです(書籍『オブジェクト指向のこころ』より引用)。
まず、オブジェクトの状態が変化した際に実行したい処理を事前に登録します。その後、実際にオブジェクトの状態が変化すると、登録された処理が自動的に実行されます。
今回の例で表現してみましょう。
PHPUnit\Event\Test\Finished
というイベントに対してTestFinishedSubscriber
を事前に登録しました。その後、DispatchingEmitter
がPHPUnit\Event\Test\Finished
のイベントを発火させ、それによりTestFinishedSubscriber
が実際に動作し、進捗結果が表示されたのでした。
構成要素が多いと感じるかもしれませんが、Observerパターンにより「大筋の目的」と「大筋の目的に紐づいて行いたい処理」の責務をきっちり分けることができます。
「テストの実行」という大筋の目的と分けて、「テストの実行結果を出力」という処理が別のクラスで管理されていることで、コードが理解しやすくなります。大筋の目的である「テストの実行」に「進捗結果を出力」する処理を自然に組み込むために、Observerパターンを利用して実装されているのです。
「Observerパターン」については、自身がPHPerKaigi 2022で発表した資料がありますので、ぜひご参考にしていただければと思います。