0
0

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 3 years have passed since last update.

MockeryでIlluminate\Database\Eloquent\Builderを利用しているメソッドのテストを書こうとしたら __clone method called on non-objectが発生した件のメモ

Last updated at Posted at 2021-06-16

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のコンストラクタにわたすように書き換えました。

テスト(おk)
    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);
    }

0
0
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
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?