- Creating a mock
- Replacing methods
- Asserting expectations
- Order expectations
- Filtering method arguments
- Default return values
- Creating partial mocks
- Mock Closures
- Compatibility
Note: all configuration of a mock is stored in $mock, the static Mock::... methods are there to provide the user-facing API.
use Exan\Moock\Mock;
$mock = Mock::class(UserService::class);
$this->assertInstanceOf(UserService::class, $mock);use Exan\Moock\Mock;
$mock = Mock::interface(UserServiceInterface::class);
$this->assertInstanceOf(UserServiceInterface::class, $mock);use Exan\Moock\Mock;
$mock = Mock::interfaces(UserServiceInterface::class, TestInterface::class);
$this->assertInstanceOf(UserServiceInterface::class, $mock);
$this->assertInstanceOf(TestInterface::class, $mock);You can replace any public method on your mocks using the following examples
use Exan\Moock\Mock;
Mock::method($this->mock->userExists(...))->replace(fn (string $email) => $email === 'exists@mail.com');
$this->assertTrue($this->mock->userExists('exists@mail.com'));
$this->assertFalse($this->mock->userExists('doesnt@mail.com'));Returning a static value
use Exan\Moock\Mock;
Mock::method($this->mock->userExists(...))->returns(true);
$this->assertTrue($this->mock->userExists('some-email@domain.com'));Returning a sequence of values
use Exan\Moock\Mock;
Mock::method($this->mock->userExists(...))->returnsSequence([true, true, false]);
$this->assertTrue($this->mock->userExists('some-email@domain.com'));
$this->assertTrue($this->mock->userExists('some-email@domain.com'));
// 3rd item is false
$this->assertFalse($this->mock->userExists('some-email@domain.com'));Throwing an exception
use Exan\Moock\Mock;
use RuntimeException;
Mock::method($this->mock->createUser(...))->throws(RuntimeException::class);
$this->expectException(RuntimeException::class);
$this->mock->createUser(
'mail@domain.com',
'username',
'password123',
);These assertions work out of the box with both PHPUnit and Nette Tester. If neither are installed, a regular PHP assert is used.
Asserting the method not called at all.
Mock::method($this->mock->userExists(...))
->returns(true);
Mock::method($this->mock->userExists(...))
->assert()
->not()
->called();Asserting the method was called at all.
Mock::method($this->mock->userExists(...))
->returns(true);
$this->mock->userExists('my-email@domain.com');
Mock::method($this->mock->userExists(...))
->assert()
->called();Asserting the method was called exactly once.
Mock::method($this->mock->userExists(...))
->returns(true);
$this->mock->userExists('my-email@domain.com');
Mock::method($this->mock->userExists(...))
->assert()
->calledOnce();Asserting the method was not called exactly once.
Mock::method($this->mock->userExists(...))
->returns(true);
$this->mock->userExists('my-email@domain.com');
$this->mock->userExists('my-email@domain.com');
Mock::method($this->mock->userExists(...))
->assert()
->not()
->calledOnce();Asserting the method was called n times.
Mock::method($this->mock->userExists(...))
->returns(true);
$this->mock->userExists('my-email@domain.com');
$this->mock->userExists('my-email@domain.com');
Mock::method($this->mock->userExists(...))
->assert()
->calledTimes(2);Asserting the method was not called n times.
Mock::method($this->mock->userExists(...))
->returns(true);
$this->mock->userExists('my-email@domain.com');
Mock::method($this->mock->userExists(...))
->assert()
->not()
->calledTimes(2);You can assert a method was called with specific input by passing the expected arguments into with().
Mock::method($this->mock->userExists(...))
->returns(true);
$this->mock->userExists('my-email@domain.com');
Mock::method($this->mock->userExists(...))
->assert()
->with('my-email@domain.com')
->called();This can of course also be reversed.
Mock::method($this->mock->userExists(...))
->returns(true);
$this->mock->userExists('my-email@domain.com');
Mock::method($this->mock->userExists(...))
->assert()
->with('other-email@domain.com')
->not()
->called();Rather than being tied to static values, you can pass a closure as well.
Mock::method($this->mock->userExists(...))
->returns(true);
$this->mock->userExists('my-email@domain.com');
Mock::method($this->mock->userExists(...))
->assert()
->with(fn (string $email) => str_ends_with($email, '@domain.com'))
->called();If you only care about a specific argument, you can use named arguments.
$this->mock->createUser('my-email@domain.com', 'my-username', 'password');
Mock::method($this->mock->createUser(...))
->assert()
->with(email: 'my-email@domain.com', password: 'password')
->called();Of course, closures can be used here too.
$this->mock->createUser('my-email@domain.com', 'my-username', 'password');
Mock::method($this->mock->createUser(...))
->assert()
->with(
email: fn (string $email) => str_ends_with($email, '@domain.com'),
password: 'password',
)->called();string must contain @mail.com
use Exan\Moock\Args\Str;
Mock::method($this->mock->userExists(...))
->returns(true);
$this->mock->userExists('test@mail.com');
Mock::method($this->mock->userExists(...))
->assert()
->with(Str::contains('@mail.com'))
->called();string must have specific length
use Exan\Moock\Args\Str;
Mock::method($this->mock->userExists(...))
->returns(true);
$this->mock->userExists('test@mail.com');
Mock::method($this->mock->userExists(...))
->assert()
->with(Str::length(13))
->called();int|float must be less than given number
use Exan\Moock\Args\Number;
Mock::method($this->mock->getUsersByAge(...))
->returns([]);
$this->mock->getUsersByAge(50);
Mock::method($this->mock->getUsersByAge(...))
->assert()
->with(Number::lt(100))
->called();int|float must be greater than given number
use Exan\Moock\Args\Number;
Mock::method($this->mock->getUsersByAge(...))
->returns([]);
$this->mock->getUsersByAge(75);
Mock::method($this->mock->getUsersByAge(...))
->assert()
->with(Number::gt(50))
->called();To see more of these helpers, head on over to the argument validators documentation.
If you're expecting many calls to be made after each other in specific order, you may use Mock::expect
To verify the order in which methods were called on a mock, call $expect in the desired order with the given method.
$this->mock->isValidEmail('mail@domain.com');
$this->mock->userExists('mail@domain.com');
$this->mock->createUser('mail@domain.com', 'username', 'password');
// Asserting the methods are called in the right order only
Mock::verify(function ($assert): void {
$assert($this->mock->isValidEmail(...));
$assert($this->mock->userExists(...));
$assert($this->mock->createUser(...));
});Optionally attach expectations of the given arguments.
Mock::verify(function (Closure $assert): void {
$assert($this->mock->isValidEmail(...))->with('mail@domain.com');
$assert($this->mock->userExists(...)); // Argument isn't verified, just order
// Only mail and password are validated
$assert($this->mock->createUser(...))->with('mail@domain.com', password: 'password');
});You may also validate in what order methods were called between several mocks
$userService = Mock::interface(UserServiceInterface::class);
$productsService = Mock::interface(ProductServiceInterface::class);
$productsService->productExists(123);
$userService->isValidEmail('mail@domain.com');
$productsService->purchase(123, 'mail@domain.com');
Mock::verify(function (Closure $assert) use ($userService, $productsService): void {
$assert($productsService->productExists(...))->with(123);
$assert($userService->isValidEmail(...))->with('mail@domain.com');
$assert($productsService->purchase(...))->with(123, 'mail@domain.com');
});Filters restrict which arguments are allowed at runtime. Calls with disallowed input will immediately throw a RuntimeException.
To filter arguments that are allowed into a method, you can use the following.
Mock::method($this->mock->userExists(...))
->allow('my-email@domain.com')
->returns(true);
$this->assertTrue($this->mock->userExists('my-email@domain.com'));
$this->expectException(RuntimeException::class);
// Since other-email@domain.com is not allowed per the filter, the method throws a RuntimeException
$this->mock->userExists('other-email@domain.com');To filter specific args of a method, use named properties.
Mock::method($this->mock->createUser(...))
->allow(username: 'my-username');
$this->mock->createUser('my-email@domain.com', 'my-username', 'password');
$this->expectException(RuntimeException::class);
// Since username: other-username is not allowed per the filter, the method throws a RuntimeException
$this->mock->createUser('my-email@domain.com', 'other-username', 'password');You can also pass a closure instead of a straight value, or use some of the helper functions documented in the expectations section instead.
Mock::method($this->mock->userExists(...))
->allow(fn (string $email) => in_array($email, ['first@mail.com', 'second@mail.com']))
->returns(true);
$this->mock->userExists('first@mail.com');
$this->mock->userExists('second@mail.com');
$this->expectException(RuntimeException::class);
$this->mock->userExists('third@domain.com');To see the available validation helpers, head on over to the argument validators documentation.
If a method is called on a mock, but said method is not replaced and a return value is required, it will return a default value. For basic types, an arbitrary hardcoded value is used. If a class or interface is returned, a mock will be returned.
These are some of the more basic return values. These values are picked with some assumptions on what would be least problematic, without throwing exceptions. Note: you should not rely on the exact values. If you need a value to be something specific, configure the method to do so
$this->assertEquals(false, $mock->returnBool());
$this->assertEquals(true, $mock->returnTrue());
$this->assertEquals(false, $mock->returnFalse());
$this->assertEquals(123, $mock->returnInt());
$this->assertEquals(123.456, $mock->returnFloat());
$this->assertEquals([], $mock->returnArray());
$this->assertEquals([], $mock->returnIterable());
$this->assertEmpty(iterator_to_array($mock->returnGenerator()));
// These objects do not have any properties
$this->assertInstanceOf(stdClass::class, $mock->returnObject());
$this->assertInstanceOf(stdClass::class, $mock->returnStdClass());
$this->assertEquals(null, $mock->returnNull());
$this->assertEquals(null, $mock->returnMixed());
// Returning static, self, or the declaring class will result in the mock returning itself
$this->assertEquals($mock, $mock->returnStatic());
$this->assertEquals($mock, $mock->returnSelf());
// Non-mocked arbitrary instances are returned
$this->assertInstanceOf(DateTime::class, $mock->returnDateTime());
$this->assertInstanceOf(DateTimeImmutable::class, $mock->returnDateTimeImmutable());
$this->assertInstanceOf(DateInterval::class, $mock->returnDateInterval());
$this->assertInstanceOf(DatePeriod::class, $mock->returnDatePeriod());If a method has a return type of an object, a mocked instance will be returned. It will return the same mock each time, so it can be used for assertions too.
$this->assertInstanceOf(MockedClassInterface::class, $mock->returnUserService());
$mock->returnUserService()->createUser('my-email@mail.com', 'my-username', 'password');
Mock::method($mock->returnUserService()->createUser(...))
->assert()
->calledOnce();A partial mock wraps an existing object, forwarding method calls and property access to the underlying instance. This allows you to override specific behavior while leaving the rest untouched.
They can be useful when only a small part of an object needs to be controlled, though they tend to work best when used sparingly.
$this->real = new UserService();
$this->real->users = [
'first@mail.com',
'second@mail.com',
'third@mail.com',
];
$this->partial = Mock::partial($this->real);Any method not explicitly mocked will be forwarded to its original implementation.
$this->assertTrue($this->partial->userExists('first@mail.com'));
$this->assertFalse($this->partial->userExists('fourth@mail.com'));Methods can still be mocked, in which case the original implementation is bypassed selectively.
use Exan\Moock\Mock;
Mock::method($this->partial->userExists(...))
->replace(fn (string $email) => $email === 'fourth@mail.com');
$this->assertFalse($this->partial->userExists('first@mail.com'));
$this->assertTrue($this->partial->userExists('fourth@mail.com'));Properties are also synced between real & fake. Note: this does not work for properties with private(set), readonly, or final.
$this->assertEquals([
'first@mail.com',
'second@mail.com',
'third@mail.com',
], $this->partial->users);
$this->partial->users = ['fourth@mail.com'];
$this->assertEquals(['fourth@mail.com'], $this->real->users);Closures are often used as callbacks, with Mock Closures you can assert what happens with these like you would for any other class mock
$mockFn = Mock::fn();
$mockFn();
$mockFn('first arg', 'second arg');
Mock::method($mockFn)->assert()->calledTimes(2);
Mock::method($mockFn)->assert()->with('first arg')->called();Named arguments are also supported
$mockFn = Mock::fn(); // hide
$mockFn('first arg', someArg: 'some value');
Mock::method($mockFn)->assert()->with(someArg: 'some value')->called();Filters can also be applied
$mockFn = Mock::fn(); // hide
Mock::method($mockFn)->allow(someArg: 'some value');
$mockFn('first arg', someArg: 'some value');
$this->expectException(RuntimeException::class);
$mockFn('first arg', someArg: 'some other value');Out of the box, Moock will use PHPUnit or Nette Tester assertion methods if they're available. If neither are available, a regular PHP assert is used instead.
use Exan\Moock\MoockAssert;
$usedAssert = false;
MoockAssert::useAssert(function (bool $actual, bool $expected, string $message) use (&$usedAssert): void {
$usedAssert = true;
});
$mock = Mock::interface(UserServiceInterface::class);
Mock::method($mock->createUser(...))
->assert()
->called(); // This now uses our custom assert, which doesn't throw exceptions on failure
$this->assertTrue($usedAssert);