概要
Mock はできるだけ使いたくない派なんだけれど、外部API利用時なんかはどうしても必要になる時がある。
Controller の場合はやりようがあるが、 Command はちょっと迷った。
CakePHP の Command で使うようなときに Mock をするには DI を使うのがよさそう。
Controller の場合はどうするか
Controller の場合は \Cake\TestSuite\IntegrationTestTrait::controllerSpy
という用途にあったメソッドが提供されている。
https://api.cakephp.org/4.2/trait-Cake.TestSuite.IntegrationTestTrait.html#controllerSpy()
また、 Controller.initialize
のように、初期化時のフックイベントが元から用意されているので、そこに差し込むようなことも可能。
https://book.cakephp.org/4/en/controllers.html#event-list
DI がよさそうな理由
Command 用には \Cake\TestSuite\ConsoleIntegrationTestTrait
が用意されているものの、 controllerSpy に相当するようなメソッドは用意されていないようだ。
Command 関連のイベントには Console.buildCommands
が用意されているが、これはどちらかというとコマンドの名前変更等が目的なようで、このイベント内でどうにかするにはかなり無理がありそう(イベントが発火されるのも、Command のインスタンス生成前)。
自前で差し込み用のイベントを作成することも考えたが、テストのためだけに新しいイベントを作成するのは躊躇われる。
そういった理由から DI がよさそうと判断した。DI 自体は CakePHP の中ではまだ実験的な位置づけらしいので、大きな変更や使えなくなったりする可能性はあると思う。
DI でやってみる
まず Command のコンストラクタで、 Mock したいクラスを受け取るようにする。 受け取った引数をメンバ変数に格納する
<?php class HelloCommand extends Command { public $externalApiService; public function __construct(ExternalApiService $externalApiService) { parent::__construct(); $this->externalApiService = $externalApiService; } public function execute(Arguments $args, ConsoleIo $io) { // モックしたい処理 $this->externalApiService->doSomething(); } }
\App\Application::services
で、 DI コンテナに登録しておく。
Command の場合は次のようになる。
<?php public function services(ContainerInterface $container): void { parent::services($container); $container ->add(HelloCommand::class) ->addArgument(ExternalApiService::class); }
ここまでで DI されるようになったので、テストで Mock に差し替えてみる。
差し替えには \Cake\TestSuite\ContainerStubTrait::mockService
を使う。
ConsoleIntegrationTestTrait は ContainerStubTrait を実装しているので、別途 use する必要はない。
<?php class HelloCommandTest extends TestCase { public function testExecute(): void { // モックに差し替える $this->mockService(ExternalApiService::class, function () { $mock = $this->createPartialMock(ExternalApiService::class, ['doSomething']); $mock->expects($this->atLeastOnce())->method('doSomething'); return $mock; }); $this->exec('hello'); $this->assertExitCode(CommandInterface::CODE_SUCCESS); } }