laravelmockingphpunit

Mocked method is not used with Instance


Inside my Export class I have created this method:

public function getBranches()
{
    return Branch::all();
}

I want to Mock this Method. Now I have the following piece of code in a testcase:

 $mock = $this->partialMock(Export::class, function (MockInterface $mock) use ($employee) {
        $mock->shouldReceive('getBranches')->andReturn(collect([]));
    });



$this->instance(Export::class, $mock);
self::assertCount(0, $mock->getBranches()); // works fine

$export = new Export();
self::assertCount(0, $export->getBranches()); // does not work, returns number of branches in Database

I am expecting that the Export class now uses the mocked getBranches Method. But this is not happening. It does not matter if I change the method to static, the results are the same. Do you have any ideas what is missing? or what I am misunderstanding?

Currently my way is to use the $mock instead of creating a new Class.


Solution

  • I am adding this as an answer just to have clear code, but I wanted to add this as a comment.

    So, you are nearly there with your code. You are mixing or not understanding how Laravel uses a Mocked instance.

    When you mock a class, you get an object instance. Depending on what package/service you are using to mock, it may vary how it works behind the scenes, but the most used one when using Laravel is Mockery. You can also use PHPUnit's mock methods (test doubles), but it is different and "more complex" than using Mockery.

    Before I explain anything, I see you are using self::assertCount, I think you are using PHPUnit and not Pest, so it should be $this->assertCount.

    So, when you run $this->assertCount(0, $mock->getBranches());, the result of getBranches will be what you mocked, so it will return collect() (collect([]) is the same as collect()), and counting [] is 0.

    But in your other try, you are running:

    $export = new Export();
    $this->assertCount(0, $export->getBranches());
    

    And that one will not work for sure because you are never telling Export to be the mocked instance.

    Laravel uses Dependency Injection (google more about this on your own and try to learn more about it) to inject classes. So you must, somehow, tell Laravel to use the mocked instance when you want to use Export.

    Using new will never "replace" or inject the needed mock, it will always literally return a new instance of that class, so it has 0 mock on it. You have to use app(Export::class) or resolve(Export::class) (resolve is an alias of app), and then you will get the mocked instance, because you also executed $this->instance(Export::class, $mock); and that is one way of telling Laravel to give $mock back when Export is needed.

    Short story long:

    // Creates the mock
    $mock = $this->partialMock(Export::class, function (MockInterface $mock) use ($employee) {
        $mock->shouldReceive('getBranches')->andReturn(collect([]));
    });
    
    // Tells Laravel to give $mock back whenever Export is needed
    $this->instance(Export::class, $mock);
    
    // How does Laravel know that you want Export?
    $instance = app(Export::class);
    $instance = resolve(Export::class);
    
    // But also in any method that injects instances for you, for example on controllers' methods
    public function index(Request $request, Export $export)
    {
        // $export is going to be $mock
    }
    

    It also works with Facades (see an example of Mocking Facades), they are super easy to mock:

    // Imagine we have Export as a facade and we can call it like Export::getBranches()
    Export::shouldReceive('getBranches')->andReturn(collect());
    
    // Any time after the previous line that you call Export::getBranches(),
    // you will get collect() back instead of the real code behind it