Search code examples
phpmockingphpunitresetspy

PHPUnit override mock "willReturn()" statement


I'm having an issue with PHPUnit mocks, when I call method()->willReturn() on the second time it seems to maintain the first value defined on setUp:

<?php

namespace Test\Application\UseCase;

use Application\UseCase\CreateFoo;
use Application\Exceptions\RequiredValueException;
use Domain\Entity\Foo;
use PHPUnit\Framework\TestCase;

class CreateFooTest extends TestCase
{
    private CreateFoo $sut;
    private Foo $foo_spy;

    public function setUp()
    {
        $this->foo_spy = $this->createMock(Foo::class);
        $this->foo_spy->method('getBar')->willReturn('bar value');
        $this->foo_spy->method('getBaz')->willReturn('baz value');

        $this->sut = new CreateFoo;
    }

    public function test_assert_given_foo_without_bar_throws_exception()
    {
        // Arrange
        $this->expectException(RequiredValueException::class);
        $this->expectExceptionMessage('Missing Bar value.');
        $this->expectExceptionCode(500);

        $this->foo_spy->method('getBar')->willReturn(null);

        // var_dump($this->foo_spy->getBar());
        // outputs: string(bar value)
        // expected: null


        // Act, Assert
        $this->sut->execute($foo_spy);
    }

    public function test_assert_given_foo_without_baz_throws_exception()
    {
        // Arrange
        $this->expectException(RequiredValueException::class);
        $this->expectExceptionMessage('Missing Baz value.');
        $this->expectExceptionCode(500);

        $this->foo_spy->method('getBaz')->willReturn(null);

        // var_dump($this->foo_spy->getBaz());
        // outputs: string(baz value)
        // expected: null

        // Act, Assert
        $this->sut->execute($foo_spy);
    }
}

Without defining on the setUp, I have to rewrite every test the default value just to test ONE method call like this:

        $this->foo_spy->method('getBar0')->willReturn('bar value 0');
        $this->foo_spy->method('getBar1')->willReturn('bar value 1');
        $this->foo_spy->method('getBar2')->willReturn('bar value 2');
        $this->foo_spy->method('getBar3')->willReturn('bar value 3');
        $this->foo_spy->method('getBaz')->willReturn(null);

The problem is: sometimes I have 10 properties or more to test, and it leads to huge unnecessary code duplication, so I would like to write a default spy mock only once and then modify only when necessary, but as you can see when I try to "reset" the method behavior, it doesn't work as expected.


Solution

  • It looks like that every time you use method, it will create a new matcher.
    Since all of them will match, it will always select the first one.

    My solution in a similar case was to use a callback:

        public function setUp()
        {
            $this->bar_value = 'bar value';
            $this->baz_value = 'baz value';
    
            $this->foo_spy = $this->createMock(Foo::class);
            $this->foo_spy->method('getBar')->will( $this->returnCallback(function () { return $this->bar_value; } ) );
            $this->foo_spy->method('getBaz')->will( $this->returnCallback(function () { return $this->baz_value; } ) );
    
            $this->sut = new CreateFoo;
        }
    

    Now you can override $this->bar_value in the single tests, getting the result that you want.