Illuminate\Database\Eloquent\Builderを利用しているメソッドのテストを書こうとしたら謎に引っかかったのでメモを残しておく。
public function countSql(): string
{
// getBuilder() はIlluminate\Database\Eloquent\Builder を返す
$tmpBuilder = clone $this->getBuilder();
$tmpBuilder->selectRaw('count(0) as count');
return $tmpBuilder->toSql();
}
このメソッドに対して、以下様のテストを書いた。
public function test_countSql(){
$builder = \Mockery::mock(EloquentBuilder::class)->makePartial();
$builder->shouldReceive('selectRaw')->with('count(0) as count')->once();
$builder->shouldReceive('toSql')->once()->andReturn('ResultOfCountSql');
$targetClass = new $this->testClassName($builder);
$actual = $targetClass->countSql();
$this->assertIsString($actual);
$this->assertSame('ResultOfCountSql',$actual);
}
エラーメッセージは「__clone method called on non-object」で、なんでMockがcloneできないの???と悩んでいたが、Mockeryは呼び出し方によってコンストラクタを元クラスのコンストラクタを呼んだり呼ばなかったりすることを忘れていた。(よく忘れる)
全く同じ問題で困っている外人を見つけたので、下記URLと同様に対処をしてFix。
https://stackoverflow.com/questions/45623804/testing-a-method-that-clones-a-mocked-parameter
Eloquent\Builder actually contains (as a member) an instance of Query\Builder, and its magic __clone method calls clone on this underlying Query\Builder object:
(適当訳)
Eloquent\Builderは、Query\Builderのインスタンスをメンバとして持ってんよ、んで、Eloquent\Builderの__clone()メソッドは、メンバであるunderlying Query\Builder(移譲先、内部の?的な意味か?)のインスタンスをcloneしてるよ。
Since you're mocking Eloquent\Builder, it doesn't actually have an underlying $this->query member because that would be set in Eloquent\Builder's constructor, which never gets called in a fully mocked object.
(適当訳)
あんたEloquent\BuilderをMockingしてるけど、このMockは、実際には移譲先の$this->queryを持ってないでしょ。なぜなら、そのメンバはEloquent\Builderのコンストラクタでsetされるからやね。Mockするときにコンストラクタ呼ばれてねえよな?
To get around this, you need to create a partial mock of Eloquent\Builder, and tell it to run its real constructor with a mocked instance of Query\Builder:
(適当訳)
要するに、あんたはパーシャルモックを作って、実際のコンストラクタを呼び出すようにせなあかんのよ。
おk了解
というわけで、Query\BuilderのMockを作って、Eloquent\Builderのコンストラクタにわたすように書き換えました。
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Database\Query\Builder as QueryBuilder;
public function test_countSql(){
$queryBuilder = \Mockery::mock(QueryBuilder::class);
$eloquentBuilder = \Mockery::mock(EloquentBuilder::class, [$queryBuilder])->makePartial();
$eloquentBuilder->shouldReceive('selectRaw')->with('count(0) as count')->once();
$eloquentBuilder->shouldReceive('toSql')->once()->andReturn('ResultOfCountSql');
$targetClass = new $this->testClassName($eloquentBuilder);
$actual = $targetClass->countSql();
$this->assertIsString($actual);
$this->assertSame('ResultOfCountSql',$actual);
}