勉強日記

チラ裏

LaravelでPDOのモックテスト2

【Laravel】DBエラー時のシナリオのテスト - 勉強日記の続き

github.com

モチベーション

  • DBエラー時の雨の日シナリオのテストをしたい

PDOモック作成・セット側

<?php

...

trait MockPdo
{
    /**
     * コネクション名を指定し、当該コネクションのPDOをモックする
     * @param string $connectionName コネクション名
     * @return MockInterface|PDO PDOのProxy Partial Mock
     */
    protected function mockPdo(string $connectionName = null): MockInterface
    {
        $connection = DB::connection($connectionName);
        $pdo = $connection->getPdo();
        /**
         * Proxy Partial Mock
         * @var MockInterface $pdoMock
         */
        $pdoMock = Mockery::mock($pdo)->makePartial();
        // ----------------------------------------
        // Connection@setPdo($pdo)の中で
        // $this->transactions = 0;
        // されるとテストケース終了時に制御が返ってこなくなる
        // $this->transactionsの値を退避し、リフレクションで再セットする
        // ----------------------------------------
        $reflectionClass = new ReflectionClass(get_class($connection));
        $transactionsAccessor = $reflectionClass->getProperty('transactions');
        $transactionsAccessor->setAccessible(true);
        $transactions = $transactionsAccessor->getValue($connection);
        $connection->setPdo($pdoMock);
        $transactionsAccessor->setValue($connection, $transactions);
        return $pdoMock;
    }
}
  • DSNの設定等が面倒なので、Connection@getPdo()でPDOを引っこ抜いてMockeryでProxy Partial Mockを作る
  • Proxy Partial MockをConnection@setPdo($pdo)に再セットする際、Connection@transactionsメンバ変数が0になってしまう
  • Connection@transactions入れ子トランザクションの深さを管理している
  • RefreshDatabaseトレイト使用時、トランザクションの中でConnection@transactionsメンバ変数が0になってしまうと、テストケースのtearDown()で制御が帰ってこなくなることがある
    • 詳細条件不明…
  • なので、Connection@setPdo($pdo)呼び出し前のConnection@transactionsの値を退避しておき、再セットする
  • 当該メンバはprotectedなのでリフレクションを使用

PDOモックのexpectation記述側

<?php
...
    /**
     * @test
     */
    public function articles_id_DBエラー時boo()
    {
        // ----------------------------------------
        // 1. setup
        // クエリ発行時にPDOExceptionが発生するようにPDOをモック
        // ----------------------------------------
        $pdoMock = $this->mockPdo();
        $pdoMock->shouldReceive('prepare')
            ->with(Mockery::on(function (string $stmt): bool {
                return preg_match('/select.*articles/', $stmt) === 1;
            }))
            ->andThrow(new \PDOException);
        // ----------------------------------------
        // 2. action and assertion
        // ----------------------------------------
        $this->get('/articles/999')->assertSeeText('boo!');
    }
  • expectation
    • articlesテーブルにselectで問い合わせたときPDOExceptionを送出する
    • それ以外は元気に動く
  • preg_match程度ならMockery::pattern($regex)が使える