SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

PHPカンファレンス実行委員プレゼンツ PHPの最前線

身近なツールで始めるコードリーディング入門~「PHPUnit」の場合

PHPカンファレンス実行委員プレゼンツ PHPの最前線 第2回


  • X ポスト
  • このエントリーをはてなブックマークに追加

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たちも渡されているようです。

 さらに辿ると、最終的にDirectDispatcherregisterSubscriberメソッドに行き着きます(2-3)。

 DirectDispatcherクラスの$subscribersプロパティにEvent名をキー、Subscriberをバリュー(配列)として格納しています。具体的には、キーはPHPUnit\Event\Test\Finishedという文字列、バリューはTestFinishedSubscriberオブジェクトが配列として入っています。

 大事なのは、この時点ではまだプロパティにデータを追加しただけで、実際の進捗結果の出力処理は動かないということです。

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メソッドを呼び出しています。

 次にTestRunnerrunメソッドでの処理を見ていきます(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メソッドが呼び出されるのです。

イベントの発火でTestFinishedSubscriberクラスが実行される

 ここまでの流れを1つの図にしてみると、下記のようになります。

全体のシーケンス図

Observerパターン

 ここまで固有名詞による説明をしていませんでしたが、実はこの一連の処理は「Observerパターン」というデザインパターンです。初見だとどのような流れかを理解するのは難しいですが、覚えてしまうとさまざまな場面で利用されていることに気づく有用なデザインパターンです。

 Observerパターンとは「あるオブジェクトの状態が変化した際、それに依存するすべてのオブジェクトに対して自動的に通知、更新を行う」ものです(書籍『オブジェクト指向のこころ』より引用)。

 まず、オブジェクトの状態が変化した際に実行したい処理を事前に登録します。その後、実際にオブジェクトの状態が変化すると、登録された処理が自動的に実行されます。

 今回の例で表現してみましょう。

 PHPUnit\Event\Test\Finishedというイベントに対してTestFinishedSubscriberを事前に登録しました。その後、DispatchingEmitterPHPUnit\Event\Test\Finishedのイベントを発火させ、それによりTestFinishedSubscriberが実際に動作し、進捗結果が表示されたのでした。

 構成要素が多いと感じるかもしれませんが、Observerパターンにより「大筋の目的」と「大筋の目的に紐づいて行いたい処理」の責務をきっちり分けることができます。

 「テストの実行」という大筋の目的と分けて、「テストの実行結果を出力」という処理が別のクラスで管理されていることで、コードが理解しやすくなります。大筋の目的である「テストの実行」に「進捗結果を出力」する処理を自然に組み込むために、Observerパターンを利用して実装されているのです。

 「Observerパターン」については、自身がPHPerKaigi 2022で発表した資料がありますので、ぜひご参考にしていただければと思います。

次のページ
Xdebug&PhpStormで最強デバッグ

関連リンク

この記事は参考になりましたか?

  • X ポスト
  • このエントリーをはてなブックマークに追加
PHPカンファレンス実行委員プレゼンツ PHPの最前線連載記事一覧
この記事の著者

asumikam(アスミ)

 株式会社リンケージのWebアプリケーションエンジニア。PHPカンファレンス小田原 実行委員長。 カンファレンスへの参加をきっかけに、日々の学びや失敗談をアウトプットするのが好きになり、ついにはカンファレンス主催まで至る。主にPHP系やアジャイル系のカンファレンスで登壇。

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

  • X ポスト
  • このエントリーをはてなブックマークに追加
CodeZine(コードジン)
https://codezine.jp/article/detail/20106 2024/09/10 17:59

おすすめ

アクセスランキング

アクセスランキング

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー

アクセスランキング

アクセスランキング