olvlvl/phpunit-given

An alternative to PHPUnit's ReturnValueMap and ReturnCallback. A convenient solution to migrate from Prophecy.

v10.0.0 2024-11-02 02:18 UTC

This package is auto-updated.

Last update: 2024-12-02 02:40:55 UTC


README

Packagist Code Coverage Downloads

olvlvl/phpunit-given provides an alternative to PHPUnit's ReturnValueMap and ReturnCallback, as well as a convenient solution to migrate from Prophecy.

Disclaimer

In most cases ReturnCallback with match can be used effectively. Don't use this package if you're comfortable with these and don't need extra features.

$mock = $this->createMock(IntegerName::class);
$mock->method('name')->willReturnCallback(fn (Integer $int) => match (true) {
    $int < new Integer(6) => 'too small',
    $int > new Integer(9) => 'too big',
    default => 'just right';
}));
$mock = $this->createMock(IntegerName::class);
$mock->method('name')->will($this
    ->given(Assert::lessThan(new Integer(6)))->return('too small')
    ->given(Assert::greaterThan(new Integer(9)))->return('too big')
    ->default()->return('just right')
);

Usage

This is a simple example, more use cases below.

use olvlvl\Given\GivenTrait;
use PHPUnit\Framework\TestCase;

final class IntegerNameTest extends TestCase
{
    use GivenTrait; // <-- adds the method 'given'

    public function testName(): void
    {
        $mock = $this->createMock(IntegerName::class);
        $mock->method('name')->will($this
            ->given(new Integer(6))->return("six")
            ->given(new Integer(12))->return("twelve")
            ->default()->throw(LogicException::class)
        );

        $this->assertEquals("six", $mock->name(new Integer(6)));
        $this->assertEquals("twelve", $mock->name(new Integer(12)));

        $this->expectException(LogicException::class);
        $mock->name(new Integer(99));
    }
}

Installation

composer require olvlvl/phpunit-given

Motivation

Coming from Prophecy, C# Moq, Golang Mock, or Kotlin Mockk, one would expect at least one of the following examples to work, but they do not.

$mock = $this->createMock(IntegerName::class);
$mock
    ->method('name')
    ->with(new Integer(6))
    ->willReturn("six");
$mock
    ->method('name')
    ->with(new Integer(12))
    ->willReturn("twelve");

// the next line crashes with: Expectation failed
$this->assertEquals("six", $mock->name(new Integer(6)));
$mock = $this->createMock(IntegerName::class);
$mock
    ->method('name')
    ->with(new Integer(6))->willReturn("six");
    // the next line crashes with: Method parameters already configured
    ->with(new Integer(12))->willReturn("twelve");

$this->assertEquals("six", $mock->name(new Integer(6)));

To return a value given certain arguments, one is expected to use ReturnValueMap or ReturnCallback. ReturnValueMap seems simple enough, but because it looks for exact matches it fails when objects are included in the arguments, unless they are the same instances. Besides, ReturnValueMap does not support constraints, you can forget doing anything fancy with it. That leaves us with ReturnCallback, which can be used effectively with match but requires the introduction of logic in the test, a practice that is discouraged.

$mock = $this->createMock(IntegerName::class);
$mock->method('name')->willReturnCallback(fn (Integer $int) => match ($int) {
    new Integer(6) => 'six',
    new Integer(12) => 'twelve',
    default => throw new Exception
}));

My motivation for creating olvlvl/phpunit-given, is to have an alternative to ReturnValueMap and ReturnCallback, that looks similar to what we find in other testing frameworks, and that allows easy migration from Prophecy.

Some PHPUnit issues, for reference:

Use cases

Comparing with objects

ReturnValueMap doesn't work with objects because it uses strict equality when comparing arguments. The following code throws a TypeError exception because ReturnValueMap cannot find a match and defaults to a null value.

$mock = $this->createMock(IntegerName::class);
$mock->method('name')->will($this->returnValueMap([
    [ new Integer(6), "six" ],
    [ new Integer(12), "twelve" ],
]));

$mock->name(new Integer(6)); // throws TypeError

olvlvl/phpunit-given substitutes values with Assert::equalTo() and compares arguments using constraints. Having objects in the arguments is not a problem.

$mock = $this->createMock(IntegerName::class);
$mock->method('name')->will($this
    ->given(new Integer(6))->return("six")
    ->given(new Integer(12))->return("twelve")
);

$this->assertEquals("six", $mock->name(new Integer(6)));
$this->assertEquals("twelve", $mock->name(new Integer(12)));

Note: You can use Assert::identicalTo() to check for the same instance.

Using constraints

We established that values are substituted with Assert::equalTo() internally. Instead of values, you can also use constraints:

$mock = $this->createMock(IntegerName::class);
$mock->method('name')->will($this
    ->given(Assert::lessThan(new Integer(6)))->return('too small')
    ->given(Assert::greaterThan(new Integer(9)))->return('too big')
    ->default()->return('just right')
);

$this->assertEquals("too small", $mock->name(new Integer(5)));
$this->assertEquals("too big", $mock->name(new Integer(10)));
$this->assertEquals("just right", $mock->name(new Integer(6)));
$this->assertEquals("just right", $mock->name(new Integer(9)));

Migrating from Prophecy

olvlvl/phpunit-given is a convenient solution to migrate from Prophecy because the code is quite similar:

$container = $this->prophesize(ContainerInterface::class);
$container->has('serviceA')->willReturn(true);
$container->has('serviceB')->willReturn(false);
$container = $this->createMock(ContainerInterface::class);
$container->method('has')->will($this
    ->given('serviceA')->return(true)
    ->given('serviceB')->return(false)
);

throw() is an alternative to willThrow(), and you can mismatch return() and throw():

$container = $this->prophesize(ContainerInterface::class);
$container->get('serviceA')->willReturn($serviceA);
$container->get('serviceB')->willThrow(new LogicException());
$container = $this->createMock(ContainerInterface::class);
$container->method('get')->will($this
    ->given('serviceA')->return($serviceA)
    ->given('serviceB')->throw(LogicException::class)
);

Contrary to Prophecy, olvlvl/phpunit-given does not return null by default, instead it throws an exception:

$mock = $this->createMock(IntegerName::class);
$mock->method('name')->will($this
    ->given(new Integer(6))->return("six")
    ->given(new Integer(12))->return("twelve")
);

$mock->name(new Integer(13)); // throws an exception
LogicException : Unexpected invocation: Test\olvlvl\Given\Acme\IntegerName::name(Test\olvlvl\Given\Acme\Integer Object (...)): string, didn't match any of the constraints: [ [ is equal to Test\olvlvl\Given\Acme\Integer Object &000000000000000c0000000000000000 (
'value' => 6
) ], [ is equal to Test\olvlvl\Given\Acme\Integer Object &00000000000001af0000000000000000 (
'value' => 12
) ] ]

Continuous Integration

The project is continuously tested by GitHub actions.

Tests Static Analysis Code Style

Code of Conduct

This project adheres to a Contributor Code of Conduct. By participating in this project and its community, you're expected to uphold this code.

Contributing

See CONTRIBUTING for details.

License

olvlvl/phpunit-given is released under the BSD-3-Clause.