0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

PHPUnitでControllerのプロパティにモックを持たせる方法

Posted at

はじめに

CakePHP の Controller クラスの initialize メソッドで、プロパティにインスタンスを持たせており、テスト実行時にはモックを持たせたかったのですが、うまくいかず詰まりました

  • 問題
    1. Controller のテストを実行するとモックではなく実処理が走る問題
    2. Controller のテストを実行すると private プロパティが上書きできない問題
      • error: [Error] Cannot access private property Controller::$property in Controller.php on line xxx

結論

EventManager::instance()->on() で Controller->initialize() を検知して controller オブジェクトを取得し、  
private プロパティを Reflection クラスで無理やり上書き可能にしてモックを差し込めば OK

tests/TestCase/Controller/Api/V1/UsersControllerTest.php
class UsersControllerTest extends TestCase
{
    use IntegrationTestTrait;

    public function setUp(): void
    {
        parent::setUp();

        // CSRF コンポーネントのトークンミスマッチによるテスト失敗を防ぐ
        // <https://book.cakephp.org/4/ja/development/testing.html#csrfcomponent-securitycomponent>
        $this->enableCsrfToken();

        // 管理者でログイン
        $this->session([
            'Auth' => [
                'User' => [
                    'id' => 'dbdc8d4c-cf7d-4833-b755-4b69e97561f3',
                    'role' => 'admin',
                ],
            ],
        ]);

        // JSON形式でアクセス
        $this->configRequest(['headers' => ['Accept' => 'application/json']]);
    }

    public function test_ユーザー削除でエラーにならないこと(): void
    {
        // Arrange
        $id = '00676011-5447-4eb1-bde1-001880663af3';
        $mockUserDeleteUseCase = $this->createMock(UserDeleteUseCase::class);
        $mockUserDeleteUseCase->expects($this->once())->method('handle');
        $this->overridePrivatePropertyWithMock(propertyName: 'userDeleteUseCase', mockObject: $mockUserDeleteUseCase);

        // Act
        $this->delete("/api/v1/users/${id}");

        // Assert
        $this->assertResponseCode(200);
        $this->assertEquals('null', (string)$this->_response->getBody());
    }

    private function overridePrivatePropertyWithMock(
        string $propertyName,
        \PHPUnit\Framework\MockObject\MockObject $mockObject
    ): void {
        // グローバルイベントマネージャーで Controller->initialize() を検知
        // <https://book.cakephp.org/4/ja/core-libraries/events.html#id4>
        \Cake\Event\EventManager::instance()->on(
            'Controller.initialize',
            function (\Cake\Event\EventInterface $event) use ($propertyName, $mockObject) {
                // コントローラのオブジェクト取得
                // <https://book.cakephp.org/4/ja/core-libraries/events.html#id12>
                $controller = $event->getSubject();

                // Reflection クラスから Controller クラスのプロパティ情報を取得
                $property = (new \ReflectionClass($controller))->getProperty($propertyName);

                // privateプロパティへのアクセスを許可
                $property->setAccessible(true);

                // プロパティの値をモックで上書き
                $property->setValue($controller, $mockObject);
            }
        );
    }
}

バージョン

  • PHP
    • 8.0.10
  • CakePHP
    • 4.3.0
  • PHPUnit
    • 9.5.10

状況

プロダクトコード

ユーザー削除APIで、以下の雰囲気のコードを書いていました

src/Controller/Api/V1/UsersController.php
class UsersController extends AppController
{
    private CakePHPUserRepository $userRepository;
    private UserDeleteUseCase $userDeleteUseCase;

    public function initialize(): void
    {
        parent::initialize();

        // テスト時のモック用にプロパティのチェック
        $this->userRepository = $this->userRepository ?? new CakePHPUserRepository();
        $this->userDeleteUseCase = $this->userDeleteUseCase ?? new UserDeleteUseCase($this->userRepository);
    }

    public function delete(): void
    {
        $userId = $this->request->getParam('userId');
        $this->userDeleteUseCase->handle($userId); // ここをモック化したい!
        $this->viewBuilder()->setClassName('Json')->setOption('serialize', []);
    }
}

テストコード

以下のテストコードでは実処理が動いていました。。。

tests/TestCase/Controller/Api/V1/UsersControllerTest.php
class UsersControllerTest extends TestCase
{
    use IntegrationTestTrait;

    // (略)

    public function test_ユーザー削除でエラーにならないこと(): void
    {
        // Arrange
        $id = '00676011-5447-4eb1-bde1-001880663af3';
        $mockUserDeleteUseCase = $this->createMock(UserDeleteUseCase::class);
        $mockUserDeleteUseCase->expects($this->once())->method('handle');
        // $this は controller ではなかった!
        $this->userDeleteUseCase = $mockUserDeleteUseCase;

        // Act
        $this->delete("/api/v1/users/${id}");

        // Assert
        $this->assertResponseCode(200);
        $this->assertEquals('null', (string)$this->_response->getBody());
    }
}

ということで、上記の結論となります

おわりに

Controller のテストでかなりつまずき、時間を吸われてしまったので、気が滅入りそうでした
(CakePHP の記事が少なめ。。。)
この記事が他のエンジニアの助けになれば幸いです

参考

  • Controller の initialize でのプロパティの差し替えは、こちらの記事に助けられました

https://www.fixes.pub/program/171563.html

  • private プロパティの上書きは、こちらの記事に助けられました

https://qiita.com/sola-msr/items/c39321fd87c404b9cb19

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?