DEV Community

Recca Tsai
Recca Tsai

Posted on • Originally published at recca0120.github.io

PHPUnit: Test Closures with Mockery::spy

Originally published at recca0120.github.io

When a function accepts a callback parameter, how do you verify in PHPUnit that the callback was actually called, with the correct arguments and the right number of times?

What's Wrong with the Naive Approach

Given this function:

function executeCallback(callable $fn): void
{
    $fn('foo');
}
Enter fullscreen mode Exit fullscreen mode

The most intuitive approach is to put assertions inside the closure:

class ExampleTest extends TestCase
{
    public function test_closure(): void
    {
        $callback = function (string $input) {
            self::assertEquals('foo', $input);
        };
        executeCallback($callback);
    }
}
Enter fullscreen mode Exit fullscreen mode

This verifies the argument, but if executeCallback never calls $callback, the test still passes because the assertion is never executed.

Using Mockery::spy to Verify Closures

According to this PR, Mockery supports spying on closures directly:

composer require --dev mockery/mockery
Enter fullscreen mode Exit fullscreen mode
use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration;
use PHPUnit\Framework\TestCase;

class ExampleTest extends TestCase
{
    use MockeryPHPUnitIntegration;

    public function test_closure(): void
    {
        /** @var \Mockery\Mock|callable $callback */
        $callback = Mockery::spy(function() {
        });

        executeCallback($callback);

        $callback->shouldHaveBeenCalled()->with('foo');
    }
}
Enter fullscreen mode Exit fullscreen mode

If the callback is never called, shouldHaveBeenCalled() will fail the test. You can also verify the call count:

$callback->shouldHaveBeenCalled()->with('foo')->twice();
Enter fullscreen mode Exit fullscreen mode

The test structure follows the 3A pattern (Arrange-Act-Assert), which is more readable than embedding assertions inside the closure -- and correctly catches the case where the callback is never called at all.

Top comments (0)