olvlvl / phpunit-given
An alternative to PHPUnit's ReturnValueMap and ReturnCallback. A convenient solution to migrate from Prophecy.
Requires
- php: >=8.1
- phpunit/phpunit: ^10.5
Requires (Dev)
- phpstan/phpstan: ^1.12
README
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:
- Feature similar to withConsecutive(), but without checking order
- Improvements on withConsecutive with return
- Remove withConsecutive()
- Symphony: Remove occurrences of withConsecutive()
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.
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.