猫でもわかるWebプログラミングと副業

本業エンジニアリングマネージャー。副業Webエンジニア。Web開発のヒントや、副業、日常生活のことを書きます。

PHPのモックフレームワークMockeryでprotectedメソッドをモックする

f:id:yoshiki_utakata:20181004212834j:plain

PHPのモックフレームワークでprotectedメソッドをモックしたくなる場合があります。例えば、

<?php

/**
 * テスト対象のクラス
 */
class Repository {

  private $database;

  /** コンストラクタDI */
  public function __construct($database) {
    $this->database = $database;
  }

  public function save($title, $description) {
    $createdAt = $this->now();
    $database->store($title, $description, $createdAt);
  }

  protected function now() {
    return now();
  }
}

このようなクラスがあったとします。そして、Mockeryを使って以下のようなテストを書きたいです。

<?php

$databaseMock = Mockery:mock(Database::class);
$databaseMock->souldReceive('store')->once()
  ->with('タイトル', '説明', $今の時間);

しかしここで問題が起こります。 $createdAtnow() が入るので、テストの実行時間によって変わってしまいます。なので簡単にアサーションできません。

今回は now() という単純な内容なので、 $createdAt に関してはチェックしない、という方法もありますが、実際にはチェックしたくなる場面は出てくるでしょう。そういった場合に、以下のようにテスト対象クラスのパーシャルモックを作成し、protectedメソッドを上書きすることができます。

<?php

$now = 適当な時間
$databaseMock = Mockery:mock(Database::class);
$databaseMock->souldReceive('store')->once()
  ->with('タイトル', '説明', $now);

$target = Mockery::mock(Repository::class, [$databaseMock])
  ->makePartial() // パーシャルモック(一部のメソッドだけを置き換える)とする
  ->shouldAllowMockingProtectedMethods(); // これでprotectedがモックできる

$target->shouldReceive('now')->andReturn($now);

$target->save('タイトル', '説明');

本当にこのテスト方法でいいのかという話

ただ、やはりこのテストはあまり良くありません。テスト対象のクラスをパーシャルモックにしているのであまり良くないというのもありますが、一番大きいのは「なぜかオーバーライド可能なnowメソッドができてしまう」という点です。また、このようなクラスを作るたびにnowメソッドが生えてしまうのもなんか微妙ですよね。

そこで、今回であれば「時刻」なので、時刻関連を扱うライブラリを導入するのが良いかと思います。例えば CarbonChronos といったライブラリです。

どちらのライブラリも「テスト用に now を固定する機能は持っていると思うので、

<?php

/**
 * テスト対象のクラス
 */
class Repository {

  private $database;

  /** コンストラクタDI */
  public function __construct($database) {
    $this->database = $database;
  }

  public function save($title, $description) {
    $createdAt = Carbon::now();
    $database->store($title, $description, $createdAt);
  }
}
<?php

$now = 適当な時刻;
Carbon::nowを固定($now);

$databaseMock = Mockery:mock(Database::class);
$databaseMock->souldReceive('store')->once()
  ->with('タイトル', '説明', $now);

$target = new Repository($databaseMock);
$target->save('タイトル', '説明');

こうなり、テストも本体のコードもシンプルになるかと思います。

Mockeryがprotectメソッドをデフォルトでモックできないようになっているのにもそれなりの理由がありますので、できれば shouldAllowMockingProtectedMethods を使わない形でテストを書いたほうが良いでしょう。

参考

詳細! PHP 7+MySQL 入門ノート

詳細! PHP 7+MySQL 入門ノート

  • 作者:大重 美幸
  • 出版社/メーカー: ソーテック社
  • 発売日: 2016/07/01
  • メディア: 単行本

パーフェクトPHP

パーフェクトPHP