Search code examples
phpunit-testingphpunitmockery

Set up multiple return values with Mockery and hard dependency


Consider the following classes reproducing my real use-case:


namespace App;

class Foo
{
    public function greeting(): string
    {
        return (new Greeting())->welcome();
    }
}

namespace App;

class Greeting
{
    public function welcome(): string
    {
        return 'hello world';
    }
}

And considering this test class which is using Mockery (version 1.3.4) to overload the hard dependency in the Foo class (the new Greeting()):


namespace Test;

use App\Foo;
use App\Greeting;
use Mockery;
use PHPUnit\Framework\TestCase;

class FooTest extends TestCase
{
    public function testGreeting(): void
    {
        $greetingMock = Mockery::mock('overload:' . Greeting::class);
        $greetingMock->shouldReceive('get')
            ->twice()
            ->andReturn(
                'foo',
                'bar'
            );

        $foo = new Foo();

        echo $foo->greeting() . PHP_EOL;
        echo $foo->greeting() . PHP_EOL;
    }
}

I would like to know why the output is:

foo
foo

Instead of being:

foo
bar

Am I misreading the documentation about the andReturn method?:

It is possible to set up expectation for multiple return values. By providing a sequence of return values, we tell Mockery what value to return on every subsequent call to the method


Solution

  • Thanks for your question.

    You might have come into troubled waters by applying two (new?) things at once and then falling between the two.

    You should actually see your test fail. And then go with the failure to locate any problems with the expectations.

    That does require writing the test step by step for the expectations you have for the testing framework and the mocking library in use before you can use that again to formulate your expectations. Otherwise things can easily grow out of proportions and then it can easily become a "not seeing the wood for the trees" situation.

    return (new Greeting())->welcome();
    

    You can then answer the question whether or not you have misread something in the documentation yourself as well. That is the essence of a test and how you should enable yourself to make use of a testing framework - otherwise it's not worth for your needs.


    Next to this introductory, more general comment, there are multiple errors in your code that prevent effective mocking and testing. Lets take a look, maybe by going through some potential misunderstandings can be cleared up as well:

    $greetingMock->shouldReceive('get')
    

    The method name is welcome not get. You should have seen an error about that:

    Mockery\Exception\BadMethodCallException : Method App\Greeting::welcome() does not exist on this mock object

    With a working example though you may learn that the expectations configured Mockery are not effective by default. Which would be easy to find out by turning twice() into a never() and seeing that calling $foo->greeting() does not make the test fail.

    Again this might be just an incomplete example in the question but this remains unknown to me then. So double check you have expectations "online" on your end as otherwise you would not know that twice() would work or not.

    Why do I put the focus on twice()?

    Well because as you shared with your question you already found out that

    andReturn(A, B)
    

    only returns A, which according to the documentation means it is the first time the method is called.

    With expectations enabled, this would be revealed directly:

    Mockery\Exception\InvalidCountException : Method welcome() from App\Greeting should be called exactly 2 times but called 1 times.

    So perhaps expectations in general are not working on your end yet? So perhaps double check you have your declarations active. See the note at the very top of Mockery Expectation Declarations.

    With expectations enabled it may become obvious that the method is not called twice but only once and it therefore correctly applies with addReturn returning for the first call, only.

    Mockery\Exception\InvalidCountException : Method welcome() from App\Greeting should be called exactly 2 times but called 1 times.

    Now a back to your asking in the original question:

    Am I misreading the documentation about the andReturn method?

    No, you're reading it fine.

    Its just that your expectation that the method would be called twice is wrong.

    It is called only once. This is written in your example code as well:

    return (new Greeting())->welcome();
    

    On each (new) Greeting, welcome is called once - not twice.

    Enforcing a test to fail first by using never instead of twice for example is an easy technique to the rough edges out of "non-working test" fast.

    You might have learned that as a kid while playing:

    If something works, try to break it.

    In testing, break things first. You actually want to show what the boundaries of "working" are.


    Therefore if a test fails and it is unclear why it fails, the test is not of much use.

    For unit-tests it is said specifically that the test is also badly written, because there should be only one reason a single test fails, so that it is clear when it is getting red what the issue is.

    A test like the test in question was far away from ready. Some expectations were formulated, but never asserted.

    Putting a simplified and working example into a question here on Stackoverflow may already help with that.

    Instead you created intermediate code within a test-method to test for something differently, with expectations just as comments not asserting automatically.

    Next time, just isolate what you'd like to test in a far smaller, own test-method first. Get that test done first then, then continue with the other test-method.

    Document your own understanding incl. your preconditions to have a test work. Its your test. And it's easier to start small and then change instead of writing the whole test upfront.