How to create test doubles in a static provider in Phpunit?
Asked Answered
E

1

6

Phpunit 10 has made non-static test providers deprecated. This makes creating test doubles inside the provider problematic, because methods like createMock() or createStub() are not static. Here is a fake test demonstrating the issue :

public function provideFake(): array
{
    $someDate = $this->createMock(\DateTime::class);
    $someOtherDate = $this->createMock(\DateTime::class);
    return [
        'first test' => [$someDate],
        'second test' => [$someOtherDate],
    ];
}

/**
 * @dataProvider provideFake
 */
public function testFake(\DateTime $date): void
{
    self::assertTrue(true);
}

Running this test in Phpunit 10 / PHP 8.2 outputs :

There was 1 PHPUnit deprecation : Data Provider method provideFake() is not static

If I change the provider to be static, the result is obviously even worse :

There was 1 PHPUnit error: The data provider specified for testFake is invalid. Using $this when not in object context

I can create the test doubles inside the test itself rather than inside the provider. In this simple example, it doesn't change much because my doubles are just dummies with no set behavior.

But in real life tests, many parameters may be necessary to define this behavior. This means that the provider must output all these parameters instead of just one object, and that the test method must have a very long signature to accept them.

This makes the code much less readable in my opinion.

Is there a way to make a static provider create test doubles ?

Externalize answered 7/8, 2023 at 15:23 Comment(3)
I've been using callback functions for a loosely related use case: test receives callable $setupCallback and executes it ($foo = $setupCallback($this). I don't know if it's a good or bad approach.Dump
@ÁlvaroGonzález It seems interesting but I fail to see how to implement it. If each test cases requires a mock with a different behavior (which is the motivation to create it in the provider in the first place), do you need one callback method per test case ?Externalize
Yes, just define a different function for each data set. This function can be a simple anonymous function that calls some other method with specific parameter, should it be needed. No answers so far, so I might post mine anyway.Dump
C
4

I can't tell whether it's a good or bad approach, but I've been using callback functions for a loosely related use case. Adapting it to your example it could go like:

private function mockDate(string $value): MockObject
{
    $mock = $this->createMock(\DateTime::class);
    $mock
        ->expects($this->any())
        ->method('format')
        ->with('y-m-d')
        ->willReturn($value);
    return $mock;
}

public static function provideFake(): array
{
    return [
        'first test' => [
            'dateCallback' => static fn (self $testCase) => $testCase->mockDate('2000-12-31'),
            'expected' => '2000-12-31',
        ],
        'second test' => [
            'dateCallback' => static fn (self $testCase) => $testCase->mockDate('2099-01-01'),
            'expected' => '2099-01-01',
        ],
    ];
}

/**
 * @dataProvider provideFake
 */
public function testFake(callable $dateCallback, string $expected): void
{
    $date = $dateCallback($this);
    self::assertInstanceOf(\DateTime::class, $date);
    self::assertEquals($expected, $date->format('y-m-d'));
}

Beware that full provider runs at once before actual tests even start, so caution needs to be taken when trying to make complex arrangements. Callbacks, of course, delay their execution until test itself (that's the whole point), but a data provider is most likely the wrong place to do complex logic like database queries.

Concordance answered 8/8, 2023 at 8:50 Comment(1)
Thanks. The part I was missing was that the callback functions returned by the provider are just wrappers around a regular method that actually creates the mock. I think it's an elegant solution, the code remains short and the important parts are easily readable.Externalize

© 2022 - 2024 — McMap. All rights reserved.