6. Test Doubles
Gerard Meszaros introduces the concept of test doubles in his “xUnit Test Patterns” book like so:
Sometimes it is just plain hard to test the system under test (SUT) because it depends on other components that cannot be used in the test environment. This could be because they aren’t available, they will not return the results needed for the test or because executing them would have undesirable side effects. In other cases, our test strategy requires us to have more control or visibility of the internal behavior of the SUT.
When we are writing a test in which we cannot (or chose not to) use a real depended-on component (DOC), we can replace it with a Test Double. The Test Double doesn’t have to behave exactly like the real DOC; it merely has to provide the same API as the real one so that the SUT thinks it is the real one!
PHPUnit provides a powerful and flexible API for creating and configuring test stubs and mock objects. These test doubles are essential for unit testing, as they allow us to isolate the code under test from its dependencies and verify its behaviour without executing the code of the real collaborating objects.
A test stub replaces a real dependency and can be configured to return predefined values or throw exceptions. Test stubs give us control over indirect inputs to our system under test, enabling us to force it onto specific execution paths and test different scenarios without relying on external systems or services.
A mock object is a type of test stub that can be configured with expectations about how it will be called. Mock objects serve as observation points, enabling us to verify indirect outputs and communication between our system under test and its collaborators. By expecting that specific methods were called with the expected arguments, we can ensure that our code interacts correctly with its dependencies.
This chapter focuses exclusively on test stubs and mock objects. Other types of test double, such as dummies, fakes, and spies, are beyond the scope of this documentation.
Use a test stub when:
You need to control what a dependency returns
You are testing the logic of the SUT
The interactions with the dependency do not matter
You need to isolate the SUT from slow or unavailable dependencies
Use a mock object when:
You need to verify that methods are called
You are testing the communication between objects
The number of method calls matters
The arguments passed to methods matter
For a detailed discussion of the conceptual differences between test stubs and mock objects and when to use which, see “Testing with(out) dependencies”.
Example Code
Some of the examples in this chapter use an interface named Database and a class named Service.
These are introduced now so that we are familiar with them when they appear in the examples.
<?php declare(strict_types=1);
interface Database
{
/**
* @throws DatabaseException
*/
public function execute(string $sql, float|int|string ...$parameters): true;
/**
* @throws DatabaseException
*/
public function query(string $sql, float|int|string ...$parameters): array;
}
Explanation:
This interface defines a contract for database operations
execute()is used for write operations (INSERT,UPDATE,DELETE), returnstrueon success, and throws an exception of failurequery()is used for read operations (SELECT), returns an array of results on success, and throws an exception of failureBoth methods accept SQL strings and variadic arguments for prepared statements
<?php declare(strict_types=1);
final readonly class Service
{
private Database $database;
public function __construct(Database $database)
{
$this->database = $database;
}
/**
* @throws ServiceException
*/
public function doSomething(): bool
{
try {
$rows = $this->database->query(
'SELECT foo FROM bar WHERE baz = ?;',
'value',
);
} catch (DatabaseException) {
throw new ServiceException;
}
if ($rows !== []) {
// ...
return true;
}
return false;
}
/**
* @throws ServiceException
*/
public function doSomethingElse(): void
{
// ...
$this->database->execute(
'INSERT INTO bar (foo, baz) VALUES (?, ?);',
'value',
'another value',
);
}
}
Explanation:
Serviceis afinal readonlyclass that depends onDatabaseThe
Databasedependency is injected through the constructor, which makes the class testable in isolation from both the dependency and the database serverdoSomething()queries the database and returnstrueif rows are found,falseotherwisedoSomethingElse()executes anINSERTstatement to add data to the database
Test Stubs
What are test stubs?
A test stub provides a replacement for a real collaborating object (a dependency) that your code interacts with. Test stubs allow you to test code in isolation without executing the actual implementation of the dependency.
A test stub is a replacement for a real component on which the System Under Test (SUT) depends. This gives the test a control point for the indirect inputs of the SUT. By controlling these indirect inputs, you can force the SUT into specific execution paths that you want to verify in your test.
Use test stubs when you want to:
Decouple your code from slow or unavailable dependencies (databases, external APIs)
Provide specific return values to test different code paths
Test error handling by simulating failures
Focus on testing the logic of the SUT rather than its dependencies
Common use cases
Configuring return values
If your code depends on a service that returns data, you can use a test stub to control the results of that service without actually calling it.
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class ServiceTest extends TestCase
{
public function testDoSomethingReturnsTrueWhenQueryReturnsRows(): void
{
$database = $this->createStub(Database::class);
$database
->method('query')
->willReturn([['foo' => 'bar']]);
$service = new Service($database);
$this->assertTrue($service->doSomething());
}
}
1. Creating the test stub
$database = $this->createStub(Database::class);
What: Creates a test stub that implements the
DatabaseinterfaceHow:
createStub()generates a test stub where all methods return default values unless configured otherwiseWhy: We need an object that “looks like”
Databaseto testServicein isolation from a real database connection
Test stubs are ideal when we only need to control what the dependency returns (indirect input).
2. Configuring the test stub
$database
->method('query')
->willReturn([['foo' => 'bar']]);
What: Configures the test stub to return a specific value when
query()is calledHow:
method('query')specifies which method to configurewillReturn([['foo' => 'bar']])sets the return value to an array containing one row
Why: This simulates the scenario where the database query finds matching rows
3. Creating the system under test
$service = new Service($database);
What: Instantiates the
Serviceclass with the test stub as its dependencyHow: The test stub is passed to the constructor, satisfying the
Databasetype requirementWhy: This is the object we are actually testing and by injecting the test stub, we control the database behavior
4. Asserting the expected behavior
$this->assertTrue($service->doSomething());
What: Verifies that
doSomething()returnstrueHow:
assertTrue()fails the test if the value is not exactlytrueWhy: When the query returns rows,
doSomething()should return true
Key Concept: Indirect Input
Indirect input occurs when the system under test receives data from a dependency rather than directly from arguments passed to the tested method or function. In this test:
The test cannot directly pass data to
doSomething()as it takes no parametersInstead,
doSomething()gets its data by calling$this->database->query()The test stub provides this indirect input by returning
[['foo' => 'bar']]
Configuring exceptions
You can configure test stubs to throw exceptions, enabling you to test how your code handles errors.
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class ServiceTest extends TestCase
{
public function testExceptionIsThrownWhenSomethingGoesWrong(): void
{
$database = $this->createStub(Database::class);
$database
->method('query')
->willThrowException(new DatabaseException);
$service = new Service($database);
$this->expectException(ServiceException::class);
$service->doSomething();
}
}
1. Creating the test stub
$database = $this->createStub(Database::class);
What: Creates a test stub that implements the
DatabaseinterfaceHow:
createStub()generates a test stub where all methods return default values unless configured otherwiseWhy: We need an object that “looks like”
Databaseto testServicein isolation from a real database connection
Test stubs are ideal when we only need to control what the dependency returns (indirect input).
2. Configuring the test stub
$database
->method('query')
->willThrowException(new DatabaseException);
What: Configures the test stub to throw a specific exception when
query()is calledHow:
method('query')specifies which method to configurewillThrowException(new DatabaseException)sets the exception to be thrown
Why: This simulates the scenario where an error occurs while querying the database
3. Configuring the expectation
$this->expectException(ServiceException::class);
What: Configures the test to only be successful if a
ServiceExceptionis thrownHow: We pass the name of the exception we expect to
expectException()Why: We want to test that a
DatabaseExceptionthrown by aDatabaseimplementation results in aServiceExceptionbeing thrown
4. Creating the system under test
$service = new Service($database);
What: Instantiates the
Serviceclass with the test stub as its dependencyHow: The test stub is passed to the constructor, satisfying the
Databasetype requirementWhy: This is the object we are actually testing and by injecting the test stub, we control the database behavior
5. Invoking what we want to test
$service->doSomething();
Reference
Creating test stubs
createStub()
Creates a test stub for the specified interface (or extendable class).
$stub = $this->createStub(InterfaceName::class);
All methods of the original type are replaced with an implementation that returns an automatically generated value that satisfies the method’s return type declaration without calling the original method. These methods are referred to as “doubled methods” or “stubbed methods”.
Doubled methods can be configured using the methods described below.
Limitation: final classes
Please note that final classes cannot be doubled.
Limitation: final, private, and static methods
Please note that final, private, and static methods cannot
be doubled. They are ignored by PHPUnit’s test double functionality and
retain their original behavior except for static methods which will
be replaced by a method throwing an exception.
Limitation: Enumerations
Enumerations (enum) are final classes and therefore cannot be
doubled.
createStubForIntersectionOfInterfaces()
Creates a test stub for an intersection of interfaces.
$stub = $this->createStubForIntersectionOfInterfaces(
[InterfaceA::class, InterfaceB::class]
);
This is useful when you need to replace an object that implements multiple interfaces.
createConfiguredStub()
Creates a test stub with methods already configured to return specific values.
$stub = $this->createConfiguredStub(
InterfaceName::class,
[
'methodOne' => 'return value one',
'methodTwo' => 'return value two',
]
);
// $stub->methodOne() will return "return value one"
// $stub->methodTwo() will return "return value two"
This is a convenience method for simple cases.
getStubBuilder()
The getStubBuilder() method provides a fluent API for creating test stubs.
It should only be used for edge cases that are not supported by the simpler createStub() or createStubForIntersectionOfInterfaces() methods.
Use getStubBuilder() only when you need advanced configuration such as:
Specifying a custom class name for the test stub
Enabling the original constructor with custom arguments
Creating partial test stubs (only doubling specific methods)
Doubling properties that do not declare property hooks
Controlling clone behavior
Disabling automatic return value generation
The getStubBuilder(string $type) method returns an object that can be used to configure and subsequently perform the creation of a test stub for the specified interface (or extendable class).
The object returned by getStubBuilder() has, among other methods, a method named getStub().
This creates and returns the configured test stub.
This method must be called last in the fluent API’s method call chain.
The following methods can be used on the object returned by getStubBuilder() to configure the creation of the test stub:
setStubClassName(string $name)
Specifies a custom class name for the generated test stub class.
Note
The specified class name must not already exist.
onlyMethods(array $methods)
Specifies which methods should be doubled (stubbed). Methods not in this list will retain their original implementation, creating a partial double.
Note
All specified methods must exist in the class.
doubleProperties(array $properties)
Specifies properties that do not declare property hooks for which property hooks should be doubled (see Properties without Hooks).
Note
A property can only be doubled when it exists, is public, is not static, is not readonly, is not final, and declares a type.
setConstructorArgs(array $arguments)
Specifies the arguments to pass to the constructor when enableOriginalConstructor() (see below) is used.
disableOriginalConstructor()
This disables the invocation of the original constructor. This is useful when you want to create a partial double and the constructor either has side effects or requires dependencies that you do not want to provide.
Note
createStub(), createConfiguredStub(), createMock(), and createConfiguredMock() create test doubles without invoking the original constructor when they are used to create a test double for an extendable class.
enableOriginalConstructor()
Enables the invocation of the original constructor. Use this with setConstructorArgs() (see above) to pass required arguments.
Note
This is the default behaviour.
The enableOriginalConstructor() method only exists in case you want to explicitly indicate in your test code that you are relying on this behaviour.
disableOriginalClone()
Disables the invocation of the original __clone() method when the test stub is cloned.
Note
The original __clone() method is not called for test doubles for extendable classes created by createStub(), createConfiguredStub(), createMock(), and createConfiguredMock().
enableOriginalClone()
Enables the invocation of the original __clone() method when the test stub is cloned.
Note
This is the default behaviour.
The enableOriginalClone() method only exists in case you want to explicitly indicate in your test code that you are relying on this behaviour.
enableAutoReturnValueGeneration()
Enables automatic generation of return values for doubled methods that do not have explicit return value configuration.
Note
This is the default behaviour.
The enableAutoReturnValueGeneration() method only exists in case you want to explicitly indicate in your test code that you are relying on this behaviour.
disableAutoReturnValueGeneration()
Disables automatic generation of return values.
When disabled, stubbed methods without explicit configuration will return null or throw an exception depending on the declared return type.
Configuring return values
willReturn()
Configures a method to return a specific value.
$stub = $this->createStub(InterfaceName::class);
$stub
->method('doSomething')
->willReturn('result');
// $stub->doSomething() always returns "result"
The return value must be compatible with the method’s return type declaration.
willReturn() can be used to configure a method to return different values on consecutive calls:
$stub = $this->createStub(InterfaceName::class);
$stub
->method('doSomething')
->willReturn('first result', 'second result');
// $stub->doSomething() returns "first result" when it is invoked for the first time
// $stub->doSomething() returns "second result" when it is invoked for the second time
// $stub->doSomething() will trigger an error when it is invoked more than twice
If a method is configured to return different values on consecutive calls, it can only be invoked a number of times equivalent to the number of configured return values.
willReturnSelf()
Configures a method to return the test stub object itself.
$stub = $this->createStub(InterfaceName::class);
$stub
->method('doSomething')
->willReturnSelf();
This is useful for testing fluent interfaces.
willReturnArgument()
Configures a method to return one of its arguments.
$stub = $this->createStub(InterfaceName::class);
$stub
->method('doSomething')
->willReturnArgument(0);
// $stub->doSomething('some value') returns 'some value'
The argument index is zero-based.
willReturnMap()
Configures a method to return different values based on the arguments it receives.
$stub = $this->createStub(InterfaceName::class);
$stub
->method('doSomething')
->willReturnMap([
['foo', 'bar', 'baz'],
['one', 'two', 'three'],
]);
// $stub->doSomething('foo', 'bar') returns 'baz'
// $stub->doSomething('one', 'two') returns 'three'
Each inner array contains the method arguments followed by the return value.
Each parameter position in the value map can be either a literal value (matched with ===)
or a PHPUnit constraint:
$stub = $this->createStub(InterfaceName::class);
$stub
->method('doSomething')
->willReturnMap([
[$this->greaterThan(5), 'large'],
[$this->lessThanOrEqual(5), 'small'],
]);
// $stub->doSomething(10) returns 'large'
// $stub->doSomething(3) returns 'small'
When no entry in the value map matches, null is returned.
willReturnStrictMap()
Works like willReturnMap(), but throws an ExpectationFailedException when no entry
in the value map matches the method invocation instead of silently returning null:
$stub = $this->createStub(InterfaceName::class);
$stub
->method('doSomething')
->willReturnStrictMap([
[1, 'one'],
[2, 'two'],
]);
// $stub->doSomething(1) returns 'one'
// $stub->doSomething(99) throws ExpectationFailedException
Like willReturnMap(), constraint objects can be used for parameter matching.
willReturnCallback()
Configures a method to return the result of a callback function.
$stub = $this->createStub(InterfaceName::class);
$stub
->method('doSomething')
->willReturnCallback(
static fn(string $input) => strtoupper($input)
);
// $stub->doSomething('string') returns 'STRING'
The callback receives the method arguments and can implement complex logic.
Get-Hooked Properties
PHP 8.4 introduced the language feature of get-hooked properties.
The example below shows an interface that declares a get-hooked property:
<?php declare(strict_types=1);
interface InterfaceWithGetHookedProperty
{
public string $property { get; }
}
The behaviour of the get-hooked property property can be configured like so:
<?php declare(strict_types=1);
use PHPUnit\Framework\MockObject\Runtime\PropertyHook;
use PHPUnit\Framework\TestCase;
final class GetHookedPropertyStubExampleTest extends TestCase
{
public function testExample(): void
{
$stub = $this->createStub(
InterfaceWithGetHookedProperty::class,
);
$stub
->method(PropertyHook::get('property'))
->willReturn('value');
$this->assertSame('value', $stub->property);
}
}
In the example shown above, PropertyHook::get('property') to specify that we want to configure the behaviour of the method that is called when the property named property is accessed for reading.
A test double implements both a get hook and a set hook for every doubled property, even when the property only declares one of them.
The interface shown above only declares a get hook for property, but the test double also implements a set hook that can be configured using PropertyHook::set('property').
Property hooks that are declared final are not doubled.
Properties that do not declare any hooks are not doubled by default: they behave like regular properties on the test double.
They can be doubled on an opt-in basis, though (see Properties without Hooks).
Properties without Hooks
PHP allows a child class to add hooks to a property that its parent class declared without hooks.
PHPUnit makes this capability available for test doubles: the doubleProperties() method of the object returned by getStubBuilder() (or getMockBuilder()) opts a property that does not declare hooks in to being doubled.
The example below shows a class with a property that does not declare hooks:
<?php declare(strict_types=1);
class ClassWithPropertyWithoutHooks
{
public string $property = 'original value';
}
The behaviour of the property property can be configured like so:
<?php declare(strict_types=1);
use PHPUnit\Framework\MockObject\Runtime\PropertyHook;
use PHPUnit\Framework\TestCase;
final class DoubledPropertyStubExampleTest extends TestCase
{
public function testExample(): void
{
$stub = $this->getStubBuilder(ClassWithPropertyWithoutHooks::class)
->doubleProperties(['property'])
->getStub();
$stub
->method(PropertyHook::get('property'))
->willReturn('value');
$this->assertSame('value', $stub->property);
}
}
A property that has been opted in with doubleProperties() supports everything that hooked properties support: PropertyHook::get() and PropertyHook::set() can be used to configure return values and callbacks as well as expectations.
Doubling a property changes how it behaves: writing to the property no longer stores a value and reading from it no longer returns the stored value.
Properties that are not listed in doubleProperties() are not affected and continue to behave like regular properties on the test double.
A property can only be doubled when it exists, is public, is not static, is not readonly, is not final, and declares a type.
doubleProperties() fails with a clear error message when one of these requirements is not met.
Configuring exceptions
willThrowException()
Configures a method to throw an exception instead of returning a value.
$stub = $this->createStub(InterfaceName::class);
$stub
->method('doSomething')
->willThrowException(new Exception);
// $stub->doSomething() throws the configured exception
Return Value Generation
Please note that methods with no configured behaviour will automatically and recursively stub return values based on the return type of the method. Consider the example shown below:
<?php declare(strict_types=1);
class C
{
public function m(): D
{
// Do something.
}
}
In the above example, the C::m() method has a return type declaration indicating that it returns an object of type D.
When a test double for C is created and no return value is configured for m() using willReturn(), for example, PHPUnit will automatically create a test double for D to be returned when m() is invoked.
Similarly, if m() had a return type declaration for a scalar type, a return value such as 0 (for int), 0.0 (for float), "" (for string), etc. would be generated.
You can disable this return value generation using the #[DisableReturnValueGenerationForTestDoubles] attribute on the test case class.
Mock Objects
What are mock objects?
A mock object is a test stub that can additionally be configured with expectations about how it should be called. Mock objects allow you to verify the communication between your System Under Test and its collaborators.
While a test stub provides control over indirect inputs (data flowing into the SUT), a mock object provides an observation point for indirect outputs (method calls from the SUT to its dependencies). Mock objects verify that the SUT communicates correctly with its dependencies.
Use mock objects when you want to:
Verify that specific methods are called
Verify the arguments passed to methods
Verify the number of times methods are called
Test the coordination and communication between objects
If you create a mock object but do not configure any expectations on it, PHPUnit will emit a notice.
This indicates you should either add expectations or use createStub() instead.
Please read “Testing with(out) dependencies” and “The Stub/Mock Intervention” for some background on why this distinction between test stubs and mock objects is critical.
Common use cases
Using a mock object for testing direct output
Use a mock object to verify that actions occur when your code triggers them in its dependencies.
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class ServiceTest extends TestCase
{
public function testDoSomethingElseExecutesInsertQuery(): void
{
$database = $this->createMock(Database::class);
$database
->expects($this->once())
->method('execute')
->with(
'INSERT INTO bar (foo, baz) VALUES (?, ?);',
'value',
'another value',
);
$service = new Service($database);
$service->doSomethingElse();
}
}
1. Creating the mock object
$database = $this->createMock(Database::class);
What: Creates a mock object that implements the
DatabaseinterfaceHow:
createMock()generates a mock object that can verify method calls and their argumentsWhy: We need to verify that
Servicecorrectly calls the database’sexecute()method
Mock objects are ideal when we need to verify how the system under test interacts with its dependencies (indirect output).
2. Setting up expectations
$database
->expects($this->once())
->method('execute')
->with(
'INSERT INTO bar (foo, baz) VALUES (?, ?);',
'value',
'another value',
);
What: Configures the mock object to expect a specific method call with specific arguments
How:
expects($this->once())- The method must be called exactly once; the test fails if called zero times or more than oncemethod('execute')- Specifies which method should be calledwith(…) - Specifies the arguments that must be passed; the test fails if different arguments are used
Why: This is the core of mock-based testing. We verify that
doSomethingElse()sends the correct SQL and parameters to theDatabaseobject
3. Creating the system under test
$service = new Service($database);
What: Instantiates the
Serviceclass with the mock object as its dependencyHow: The mock object is passed to the constructor, satisfying the
Databasetype requirementWhy: This is the object we are actually testing and by injecting the mock object, we can verify the communication between
ServiceandDatabase
4. Executing the system under test
$service->doSomethingElse();
What: Calls the method we want to test
When the test method completes, PHPUnit automatically verifies that all expectations set on the mock object were met.
Key Concept: Indirect Output
Indirect output occurs when the system under test sends data to a dependency rather than returning it directly. In this test:
doSomethingElse()returns nothing (void), there is no direct output to assertInstead, the method’s effect is calling
$this->database->execute()with specific argumentsThe mock object captures this indirect output and verifies it matches our expectations
Reference
Creating mock objects
createMock()
Creates a mock object for the specified interface (or extendable class).
$mock = $this->createMock(InterfaceName::class);
All methods can be configured with return values and expectations.
Limitation: final classes
Please note that final classes cannot be doubled.
Limitation: final, private, and static methods
Please note that final, private, and static methods cannot
be doubled. They are ignored by PHPUnit’s test double functionality and
retain their original behavior except for static methods which will
be replaced by a method throwing an exception.
Limitation: Enumerations
Enumerations (enum) are final classes and therefore cannot be
doubled.
createMockForIntersectionOfInterfaces()
Creates a mock object for an intersection of interfaces.
$mock = $this->createMockForIntersectionOfInterfaces(
[InterfaceA::class, InterfaceB::class]
);
This is useful when you need to replace an object that implements multiple interfaces.
createConfiguredMock()
Creates a mock object with methods already configured to return specific values.
$mock = $this->createConfiguredMock(
InterfaceName::class,
[
'methodOne' => 'return value one',
'methodTwo' => 'return value two',
]
);
// $mock->methodOne() will return "return value one"
// $mock->methodTwo() will return "return value two"
This is a convenience method for simple cases.
createPartialMock()
Creates a mock object for an extendable class where only the specified methods are doubled. All other methods retain their original implementation.
$mock = $this->createPartialMock(
ClassName::class,
['methodToDouble']
);
$mock
->expects($this->once())
->method('methodToDouble')
->willReturn('mocked value');
// $mock->methodToDouble() is expected to be called once
// $mock->methodToDouble() returns "mocked value"
// All other methods of ClassName execute their real code
This can be useful when you need to test a class but want to replace only specific methods (for example, methods that interact with external systems) while keeping the rest of the class’ behavior intact. The original constructor is disabled.
getMockBuilder()
The getMockBuilder() method provides a fluent API for creating mock objects.
It should only be used for edge cases that are not supported by the simpler createMock() or createMockForIntersectionOfInterfaces() methods.
The documentation and recommendations for getStubBuilder() (see above) also apply to getMockBuilder(), with two differences:
The method for specifying a custom class name for the generated mock object class is
setMockClassName()The name of the method that must be called last in the fluent API’s method call chain is
getMock()
Configuring behavior
Mock objects support all the same methods for configuring behavior (return values, exceptions) as test stubs. See the test stub reference section above for detailed examples of these methods.
Configuring expectations
Expectations define how many times and with what arguments a method should be called.
once()
The method must be called exactly once.
$mock = $this->createMock(InterfaceName::class);
$mock
->expects($this->once())
->method('doSomething');
once() is a convenience wrapper for exactly(1).
exactly(int $count)
The method must be called exactly $count times.
$mock = $this->createMock(InterfaceName::class);
$mock
->expects($this->exactly(2))
->method('doSomething');
atLeastOnce()
The method must be called at least once.
$mock = $this->createMock(InterfaceName::class);
$mock
->expects($this->atLeastOnce())
->method('doSomething');
atLeastOnce() is a convenience wrapper for atLeast(1).
Avoid using atLeastOnce()
Non-exact invocation expectations like atLeastOnce() should generally be avoided because they make test
intent less explicit. When the exact number of invocations does not matter, a test stub is usually more
appropriate than a mock object. When the number of invocations does matter, exactly() or once()
communicates intent more clearly.
See GitHub issue #6483 for details.
atLeast(int $requiredInvocations)
The method must be called at least $requiredInvocations times.
$mock = $this->createMock(InterfaceName::class);
$mock
->expects($this->atLeast(2))
->method('doSomething');
Avoid using atLeast()
Non-exact invocation expectations like atLeast() should generally be avoided because they make test
intent less explicit. When the exact number of invocations does not matter, a test stub is usually more
appropriate than a mock object. When the number of invocations does matter, exactly() communicates
intent more clearly.
See GitHub issue #6483 for details.
atMost(int $allowedInvocations)
The method must not be called more than $allowedInvocations times.
$mock = $this->createMock(InterfaceName::class);
$mock
->expects($this->atMost(2))
->method('doSomething');
Avoid using atMost()
Non-exact invocation expectations like atMost() should generally be avoided because they make test
intent less explicit. When the exact number of invocations does not matter, a test stub is usually more
appropriate than a mock object. When the number of invocations does matter, exactly() or never()
communicates intent more clearly.
See GitHub issue #6483 for details.
never()
The method must not be called.
$mock = $this->createMock(InterfaceName::class);
$mock
->expects($this->never())
->method('doSomething');
never() is a convenience wrapper for exactly(0).
any()
The method may be called zero or more times. No expectation is set on the number of invocations, so the test will not fail regardless of how many times the method is called (including not at all).
$mock = $this->createMock(InterfaceName::class);
$mock
->expects($this->any())
->method('doSomething')
->willReturn('value');
The mock object configuration shown above is equivalent to the test stub configuration shown below:
$stub = $this->createStub(InterfaceName::class);
$stub
->method('doSomething')
->willReturn('value');
Deprecation: any() is deprecated
The any() matcher, used as $this->expects($this->any()), is deprecated.
It will be removed in PHPUnit 14.
Using any() with expects() is contradictory: you are creating a mock object (designed to verify
communication between objects) while essentially saying “I don’t care if this communication happens at all”.
If you do not need to verify that a method is called, use createStub() instead of createMock().
Test stubs are the appropriate choice when you only need to control what a dependency returns
without verifying how many times it is called.
See GitHub issue #6461 for details.
with()
The with() method verifies the arguments passed to the mocked method.
$mock = $this->createMock(InterfaceName::class);
$mock
->expects($this->once())
->method('doSomething')
->with('argument 1', 'argument 2');
In the example shown above, we configure the mock object to expect exactly one call to the method doSomething().
For this single call, the arguments "argument 1" and argument 2" must be passed.
The example shown above is equivalent to the following:
$mock = $this->createMock(InterfaceName::class);
$mock
->expects($this->once())
->method('doSomething')
->with($this->equalTo('argument 1'), $this->equalTo('argument 2'));
The with() method verifies the arguments passed to the mocked method using Constraint objects.
If a value passed to with() is not a Constraint object then that value is automatically wrapped in a Constraint object that verifies equality.
A Constraint object that verifies equality can be manually created using $this->equalTo().
Arguments can also be passed to with() as named arguments.
A named argument is matched against the parameter of the mocked method that has that name.
Consider a Logger interface with a method declared as log(string $message, string $level = 'info', array $context = []):
$logger = $this->createMock(Logger::class);
$logger
->expects($this->once())
->method('log')
->with(level: 'error');
In the example shown above, the call to log() must pass 'error' as $level, while its message and its context are not verified.
Positional and named arguments can be combined, as in ->with('Payment failed', level: 'error').
A parameter that is not mentioned is not verified.
The test will fail if:
A named argument does not name a parameter of the method
A parameter is specified both by position and by name
If the method has a variadic parameter, a named argument that does not name one of the other parameters is matched against the named argument of that name that the variadic parameter collects.
Before PHPUnit 12.5.39 and PHPUnit 13.4.2, the names of named arguments passed to with() were ignored and the values were matched by position.
Deprecation: Using with() without expects() is deprecated
Using with() on a mock object without also using expects() is deprecated.
This will no longer be possible in PHPUnit 14.
The with() method is used to verify the arguments passed to a method, which is an expectation about
how the mock object is called. Using with() without expects() is contradictory: you are specifying
which arguments a method should be called with while not setting up an expectation for how many times the
method should be called. Use expects() together with with() to make the intent of your test explicit.
PHPUnit provides many constraint methods for argument verification:
Identity and Equality
identicalTo()equalTo()equalToCanonicalizing()equalToIgnoringCase()equalToWithDelta()objectEquals()
Cardinality
isEmpty()countOf()greaterThan()greaterThanOrEqual()lessThan()lessThanOrEqual()
Math
isFinite()isInfinite()isNan()
Boolean
isFalse()isTrue()
Operator
logicalAnd()logicalNot()logicalOr()logicalXor()
String
isJson()matches()matchesRegularExpression()stringContains()stringEndsWith()stringEqualsStringIgnoringLineEndings()stringStartsWith()
Traversable
arrayHasKey()containsEqual()containsIdentical()containsOnlyArray()containsOnlyBool()containsOnlyCallable()containsOnlyClosedResource()containsOnlyFloat()containsOnlyInstancesOf()containsOnlyInt()containsOnlyIterable()containsOnlyNull()containsOnlyNumeric()containsOnlyObject()containsOnlyResource()containsOnlyScalar()containsOnlyString()isList()
Type
isArray()isBool()isCallable()isClosedResource()isFloat()isInstanceOf()isInt()isIterable()isNull()isNumeric()isObject()isResource()isScalar()isString()
Filesystem
directoryExists()fileExists()isReadable()isWritable()
withAnyParameters()
The withAnyParameters() method configures an expectation that accepts any arguments:
$mock = $this->createMock(InterfaceName::class);
$mock
->expects($this->once())
->method('doSomething')
->withAnyParameters();
This is equivalent to not using with() at all, but states the intent explicitly.
with() and withAnyParameters() are mutually exclusive: only one of them may be used
for a method, and only one expectation configured for a method may verify its arguments.
Expecting calls to the same method with varying arguments in specific order
When testing code that calls the same method multiple times with different arguments, we often need to verify that:
The method is called the expected number of times
Each call receives the correct arguments
The calls happen in a specific order
Consider a service that depends on a Dispatcher to dispatch events using its dispatch() method.
These are use cases where the order in which these events are dispatched matters.
We cannot handle this scenario using with() (see above) because setting up two separate expectations using expects() for the same method does not work (the second overwrites the first).
The withParameterSetsInOrder() method solves this by allowing us to specify multiple parameter sets that must be matched in sequence:
$dispatcher = $this->createMock(Dispatcher::class);
$dispatcher
->expects($this->exactly(2))
->method('dispatch')
->withParameterSetsInOrder(
[new AnEvent],
[new AnotherEvent],
);
$service = new Service($dispatcher);
$service->doSomething();
Ordered parameter sets allow us to verify that a method is called multiple times with different arguments in a specific order. This is essential when the order of operations affects correctness or we need to verify a specific workflow or protocol.
The test will fail if:
The method is called with the wrong arguments
The calls happen in the wrong order
The method is called fewer or more times than expected
Expecting calls to the same method with varying arguments in any order
Sometimes we need to verify that a method is called multiple times with specific arguments, but the order of the calls does not matter. For example:
Logging multiple events where sequence is irrelevant
Sending notifications to multiple recipients
Recording audit entries that are independent of each other
The withParameterSetsInAnyOrder() method allows us to specify multiple parameter sets that must all be used, but can be matched in any order:
$dispatcher = $this->createMock(Dispatcher::class);
$dispatcher
->expects($this->exactly(2))
->method('dispatch')
->withParameterSetsInAnyOrder(
[new AnotherEvent],
[new AnEvent],
);
$service = new Service($dispatcher);
$service->doSomething();
Unordered parameter sets allow us to verify that a method is called multiple times with specific arguments, without enforcing a particular sequence. This is useful when:
The order of operations does not affect correctness
We want to allow implementation flexibility
We are testing code where calls might be reordered for optimization
The test will fail if:
The method is called with arguments that do not match any expected parameter set
Not all parameter sets are used (some expected calls are missing)
The method is called fewer or more times than expected
Expecting calls to the same method with varying arguments in partial order
Sometimes the order of some calls matters while the order of others does not. For example, an initialization call must happen at a specific position in the sequence, but the calls around it can occur in any order.
The withParameterSetsInPartialOrder() method allows us to specify multiple parameter sets where individual sets can be “pinned” to a specific position while the remaining sets may be matched in any order.
A parameter set is pinned by wrapping it in an array with the key pinned; its position in the argument list of withParameterSetsInPartialOrder() determines the call index (zero-based) at which it must be matched:
$dispatcher = $this->createMock(Dispatcher::class);
$dispatcher
->expects($this->exactly(3))
->method('dispatch')
->withParameterSetsInPartialOrder(
[new AnEvent],
[new AnotherEvent],
['pinned' => new FinalEvent],
);
$service = new Service($dispatcher);
$service->doSomething();
In the example above, AnEvent and AnotherEvent may be dispatched in either order for the first two calls, but FinalEvent must be the argument of the third (and last) call.
Partially ordered parameter sets allow us to combine the strictness of ordered parameter sets with the flexibility of unordered parameter sets. This is useful when:
Only some calls have a meaningful order (for example, a setup or teardown call), while others can be reordered
We want to anchor a specific call to a specific position without over-specifying the order of all other calls
The test will fail if:
A pinned parameter set is not matched at its configured position
The method is called with arguments that do not match any remaining (non-pinned) parameter set
Not all parameter sets are used (some expected calls are missing)
The method is called fewer or more times than expected
Using parameter names in parameter sets
The parameter sets shown above list the expected arguments in the order of the method’s parameters.
When only some of the parameters matter for a call, a parameter set can use the names of the parameters as array keys instead.
This works with withParameterSetsInOrder(), withParameterSetsInAnyOrder(), and withParameterSetsInPartialOrder().
Consider a Logger interface with a method declared as log(string $message, string $level = 'info', array $context = []):
$logger = $this->createMock(Logger::class);
$logger
->expects($this->exactly(2))
->method('log')
->withParameterSetsInOrder(
['level' => 'info'],
['Payment failed', 'level' => 'error'],
);
$service = new Service($logger);
$service->doSomething();
In the example shown above, the first call to log() must pass 'info' as $level, while its message and its context are not verified.
The second call must pass 'Payment failed' as $message and 'error' as $level.
An entry with an integer key is matched by position, and an entry with a string key is matched by the name of the parameter, just like positional and named arguments in a method call. A parameter that is not mentioned in a parameter set is not verified.
The test will fail if:
A key is not the name of a parameter of the method
A parameter is specified both by position and by name
If the method has a variadic parameter, a key that is not the name of one of the other parameters is matched against the named argument of that name that the variadic parameter collects.
A parameter set that uses parameter names is pinned in the same way as any other parameter set: ['pinned' => ['level' => 'error']].
A parameter set whose only key is pinned is always treated as a pinned parameter set, even when the method has a parameter named $pinned.
Before PHPUnit 13.4.2, string keys in parameter sets were ignored and the values were matched by position.
Verifying relative call order between mock object expectations
The id() and after() methods allow us to define dependencies between mock expectations, ensuring that one method is called only after another method has been called first.
id(string $id): Assigns a unique identifier to an expectationafter(string $id): Specifies that this expectation should only be verified after the expectation with the given ID has been matched
Below is a complete example demonstrating how to use id() and after() to verify that two() is called after one():
public function testTwoIsCalledAfterOne(): void
{
$mock = $this->createMock(ServiceInterface::class);
$mock
->expects($this->once())
->method('one')
->id('first-call');
$mock
->expects($this->once())
->method('two')
->after('first-call');
$mock->one();
$mock->two();
}
1. Configuring the first expectation
$mock
->expects($this->once())
->method('one')
->id('first-call');
expects($this->once())sets up an expectation that the following method should be called exactly oncemethod('one')specifies that this expectation applies to the one() methodid('first-call')assigns the unique identifier"first-call"to this expectation. This ID can be referenced by other expectations usingafter().
2. Configuring the second expectation
$mock
->expects($this->once())
->method('two')
->after('first-call');
expects($this->once())sets up an expectation thattwo()should be called exactly oncemethod('two')specifies that this expectation applies to thetwo()methodafter('first-call')creates a dependency on the expectation with ID"first-call"'"
This means:
The expectation for
two()will only be verified afterone()has been calledIf
two()is called beforeone(), that call will not count toward satisfying this expectationThe test will fail if
two()is not called afterone()has been called
3. Executing the system under test
$mock->one();
$mock->two();
The call to
one()on the mock object satisfies the first expectation and “unlocks” the second expectationThe call to
two()on the mock object satisfies the second expectation, sinceone()was already called
If two() is called before one(), that call does not count toward satisfying the after() expectation.
Attempting to register the same ID twice will cause the test to error.
Using after() with an ID that has not been registered with id() will cause the test to fail.
Deprecation: id() and after() are deprecated
The id() and after() methods are soft-deprecated since PHPUnit 13.1.
They will be hard-deprecated in PHPUnit 14 and they will be removed in PHPUnit 15.
Use an invocation journal to verify the order in which the methods of mock objects are called.
See GitHub issue #6537 for details.
Recording the order in which methods are called
The withParameterSetsInOrder(), withParameterSetsInAnyOrder(), and withParameterSetsInPartialOrder() methods shown above verify the order of multiple calls to the same method of the same mock object.
Sometimes, though, the order of calls to different methods, possibly on different mock objects, is part of the contract we want to test.
An invocation journal records the invocations of the methods of one or more mock objects, in the order in which they happen, so that we can assert that order ourselves:
createInvocationJournal(): Creates an invocation journal, an object that implements thePHPUnit\Framework\MockObject\InvocationJournalinterfacerecordInvocationsIn(InvocationJournal $journal, ?string $label = null, array $methodLabels = []): Registers$journalwith a mock object so that every invocation of one of its methods is recorded in$journalInvocationJournal::asArray(): Returns the labels of the recorded invocations, in the order in which the invocations happenedInvocationJournal::only(): Returns only those recorded invocations whose label was passed, in the order in which the invocations happened
A journal only records; it does not verify anything by itself. We assert the recorded order using an ordinary assertion.
Consider a Publisher that writes a value to a Database and then announces the change through a Dispatcher.
The event must not be announced before the data has been written, because a subscriber that reacts to the event has to be able to read the new value.
The order of these two calls is therefore part of the contract of Publisher.
Below is a complete example demonstrating how to use an invocation journal to verify that Dispatcher::dispatch() is called after Database::execute():
public function testEventIsDispatchedAfterDataHasBeenWritten(): void
{
$journal = $this->createInvocationJournal();
$database = $this->createMock(Database::class);
$dispatcher = $this->createMock(Dispatcher::class);
$database->recordInvocationsIn($journal, 'database');
$dispatcher->recordInvocationsIn($journal, 'dispatcher');
$database
->expects($this->once())
->method('execute')
->willReturn(true);
$dispatcher
->expects($this->once())
->method('dispatch');
$publisher = new Publisher($database, $dispatcher);
$publisher->publish('value');
$this->assertSame(
['database::execute()', 'dispatcher::dispatch()'],
$journal->asArray(),
);
}
1. Creating the invocation journal
$journal = $this->createInvocationJournal();
createInvocationJournal()creates a new, empty invocation journalThe journal is returned as an
InvocationJournal, an interface that only allows us to read what was recorded. Only PHPUnit writes to a journal.A single journal can be shared by any number of mock objects. This is what makes it possible to verify the order of calls across mock object boundaries.
2. Registering the journal with the mock objects
$database->recordInvocationsIn($journal, 'database');
$dispatcher->recordInvocationsIn($journal, 'dispatcher');
recordInvocationsIn()registers the journal with a mock object. From that point on, every invocation of one of that mock object’s methods is recorded.The journal records invocations, not expectations: a method is recorded when it is called, regardless of which expectations are configured for it, and regardless of whether it matches any of them.
Because both mock objects record in the same journal, their invocations are recorded on one common timeline.
The optional second argument is the label used for this mock object. It is described below.
3. Configuring the expectations
$database
->expects($this->once())
->method('execute')
->willReturn(true);
$dispatcher
->expects($this->once())
->method('dispatch');
Expectations are configured as usual. Recording is independent of them: it neither requires an expectation nor changes how one is verified.
Recording an invocation does not interfere with configuring a return value.
willReturn()and the otherwill*()methods are unaffected.
4. Exercising the system under test
$publisher = new Publisher($database, $dispatcher);
$publisher->publish('value');
The two mock objects are injected into the
Publisherobject we want to testWhile
publish()runs, each invocation of a method of a registered mock object appends an entry to the journalAn invocation is recorded before the behaviour configured for it is invoked. Invocations that are made from within a callback configured using
willReturnCallback(), for instance, are therefore recorded after the invocation that caused them.The expectations configured using
expects($this->once())are verified at the end of the test as usual.
5. Asserting the order in which the methods were called
$this->assertSame(
['database::execute()', 'dispatcher::dispatch()'],
$journal->asArray(),
);
asArray()returns the labels of all recorded invocations as a list, in the order in which the invocations happenedassertSame()compares that list with the order we expect. The test fails, with a diff of the two lists, ifdispatch()was called beforeexecute().The journal does not assert anything by itself. It only records what happened; we decide what to assert about it.
Each entry is built from the label of the mock object and the name of the method that was invoked.
The $label argument of recordInvocationsIn() is optional:
$database->recordInvocationsIn($journal);
When no label is passed, the fully-qualified name of the doubled type is used, for example Database::execute().
Pass a label explicitly when a shorter label makes the assertion easier to read, or when two mock objects for the same type record their invocations in the same journal.
Individual methods can be given a label of their own using the third argument, which maps method names to labels:
$database->recordInvocationsIn($journal, 'database', ['execute' => 'write']);
$dispatcher->recordInvocationsIn($journal, 'dispatcher', ['dispatch' => 'dispatch']);
// ...
$this->assertSame(['write', 'dispatch'], $journal->asArray());
A label configured this way replaces the entire entry: an invocation of execute() is recorded as write, not as database::write().
Methods that are not named in the map are recorded as label::method() as before.
Use this when the name of the step matters more to the reader of the test than the name of the method that implements it.
The name of a property hook is written the way it is written everywhere else in the test double API:
$double->recordInvocationsIn($journal, 'double', ['$property::get' => 'read']);
Naming a method that the mock object does not have, or giving it an empty label, is an error and is reported as such.
Because the order is verified using an ordinary assertion, we choose how strict that assertion is:
$invocations = $journal->asArray();
// the methods were called in exactly this order
$this->assertSame(['database::execute()', 'dispatcher::dispatch()'], $invocations);
// both methods were called, the order does not matter
$this->assertEqualsCanonicalizing(['database::execute()', 'dispatcher::dispatch()'], $invocations);
// 'database::execute()' happened at some point before 'dispatcher::dispatch()'
$this->assertLessThan(
array_search('dispatcher::dispatch()', $invocations, true),
array_search('database::execute()', $invocations, true),
);
A journal records every invocation of every mock object registered with it.
When only some of those invocations are of interest, only() narrows what we assert on, without changing what was recorded:
$this->assertSame(['write', 'dispatch'], $journal->only('write', 'dispatch'));
This asserts that write happened before dispatch, and says nothing about any other invocation that may have happened before, between, or after them.
It is how a relative order is expressed: the journal remains a complete record of what happened, and the assertion decides how much of it matters.
Note
Narrowing to exactly the one label that is then asserted, as in assertSame(['write'], $journal->only('write')), asserts only that write happened at all.
Pass every label whose relative order matters.
InvocationJournal implements Countable and IteratorAggregate, so assertCount() can be used to assert how many invocations were recorded, and a journal can be iterated directly:
$this->assertCount(2, $journal);
foreach ($journal as $invocation) {
// ...
}
Note
recordInvocationsIn() is only available for mock objects, not for test stubs.
The only reason to observe the order in which methods are called is to assert that order, and assertions about how a test double was used belong on a mock object.
A mock object that only records its invocations, and for which no expectations are configured, does not trigger the notice about mock objects without expectations.
Only verify an order that is part of a contract
Verify the order in which methods are called only when that order is part of a contract: when the system under test promises it, or when the interface of a collaborator documents it.
Do not pin down an order that is merely an accident of how a method happens to be written today. Such a test fails on refactorings that change no behaviour, and it tells its readers that an implementation detail is a promise.
Often, an apparent ordering requirement is really a statement about state: not “beginTransaction() was called before commit()”, but “the work was performed while a transaction was open”.
Assertions about state are more precise, and they explain why the order matters.
Methods that never return
The never return type indicates that a function or method will never return normally. Such methods either:
Throw an exception
Call
exit()ordie()Enter an infinite loop
When writing unit tests, mocking methods with a never return type requires special handling because the mock cannot actually “never return” as it must do something when called.
When you create a mock object of an interface or extendable class that has a method with a never return type, the mock object will throw a NeverReturningMethodException when that method is called.
This exception simulates the behavior of a method that never returns normally.
Consider the following scenario where we have an ErrorHandler interface with a handle() method that has a never return type:
<?php declare(strict_types=1);
interface ErrorHandler
{
public function handle(Exception $e): never;
}
<?php declare(strict_types=1);
final readonly class ErrorHandlerImplementation implements ErrorHandler
{
public function handle(Exception $e): never
{
print $e->getMessage();
exit;
}
}
<?php declare(strict_types=1);
final readonly class ServiceImplementation implements Service
{
private ErrorHandler $errorHandler;
public function __construct(ErrorHandler $errorHandler)
{
$this->errorHandler = $errorHandler;
}
public function doSomething(): void
{
try {
// ...
throw new Exception('message');
} catch (Exception $e) {
$this->errorHandler->handle($e);
}
}
}
We want to test that, when an exception is thrown during the execution of ServiceImplementation::doSomething(), the ErrorHandler::handle() method is called with an exception object of the expected type. Here is how to do that:
1. Creating the mock object
$errorHandler = $this->createMock(ErrorHandler::class);
This line creates a mock object that implements the ErrorHandler interface.
The mock object will have all the methods defined in the interface, including the handle() method with its never return type.
2. Configuring the mock object
$errorHandler
->expects($this->once())
->method('handle')
->with($this->isInstanceOf(Exception::class))
->seal();
This fluent chain configures the mock object’s behavior and expectations:
->expects($this->once()): Sets up an expectation that thehandle()method will be called exactly once during the test. If the method is not called, or called more than once, the test will fail.->method('handle'): Specifies that we are configuring thehandle()method of the mock object.->with($this->isInstanceOf(Exception::class)): Specifies that thehandle()method must be called with an argument that is an instance of theExceptionclass. This constraint validates the argument passed to the method.->seal(): Finalizes the mock configuration and prevents any further configuration changes.
3. Creating the system under test
$service = new ServiceImplementation($errorHandler);
This line creates an instance of the ServiceImplementation class, injecting the mock object we created and configured to replace the ErrorHandler dependency.
This is standard dependency injection, allowing us to test ServiceImplementation in isolation from its dependency ErrorHandler.
4. Configuring the expectation that the mocked method never returns
$this->expectException(NeverReturningMethodException::class);
This line tells PHPUnit to expect that a NeverReturningMethodException will be thrown during the test.
This is the key to testing methods with never return type:
When
$service->doSomething()is called, it will internally call$this->errorHandler->handle($e)Since
handle()has a never return type, PHPUnit throws aNeverReturningMethodExceptionBy expecting this exception, we document our assumption that the
handle()method never returns
5. Executing the system under test
$service->doSomething();
This line calls the method we want to test. The execution flow is:
doSomething()is called on the serviceInside
doSomething(), an exception is thrown and caughtThe caught exception is passed to
$this->errorHandler->handle($e)The mock object’s
handle()method is invokedPHPUnit verifies the expectation (called once with an
Exceptioninstance)PHPUnit throws
NeverReturningMethodExceptionbecause the method has aneverreturn typeThe test passes because we expected this exception
Always use $this->expectException(NeverReturningMethodException::class) when testing code that calls a mocked method with never return type.
The exception replaces the “never return” behavior: In real code, a method with never return type would call exit(), throw an exception, or loop forever.
In tests, NeverReturningMethodException simulates this by providing a controlled way to exit the method.
Verification still works: Even though an exception is thrown, PHPUnit still verifies that your expectations (like expects($this->once()) and with()) are met.
This pattern allows you to effectively test code that depends on methods that never return normally, while still being able to verify that those methods are called with the correct arguments.
Set-Hooked Properties
The example below shows an interface that declares a set-hooked property:
<?php declare(strict_types=1);
interface InterfaceWithSetHookedProperty
{
public string $property { set; }
}
Expectations for the set-hooked property property can be configured like so:
<?php declare(strict_types=1);
use PHPUnit\Framework\MockObject\Runtime\PropertyHook;
use PHPUnit\Framework\TestCase;
final class SetHookedPropertyMockExampleTest extends TestCase
{
public function testExample(): void
{
$mock = $this->createMock(
InterfaceWithSetHookedProperty::class,
);
$mock
->expects($this->once())
->method(PropertyHook::set('property'))
->with('value');
$mock->property = 'value';
}
}
In the example shown above, PropertyHook::set('property') to specify that we want to configure an expectation for the method that is called when the property named property is accessed for writing.
A test double implements both a get hook and a set hook for every doubled property, even when the property only declares one of them.
The interface shown above only declares a set hook for property, but the test double also implements a get hook for which expectations can be configured using PropertyHook::get('property').
Property hooks that are declared final are not doubled.
Properties that do not declare any hooks are not doubled by default: they behave like regular properties on the test double.
They can be doubled on an opt-in basis, though (see Properties without Hooks).
Properties without Hooks
The doubleProperties() method of the object returned by getMockBuilder() opts a property that does not declare hooks in to being doubled (see Properties without Hooks for details and limitations).
Expectations for a property that has been opted in with doubleProperties() can be configured just like expectations for a hooked property:
<?php declare(strict_types=1);
use PHPUnit\Framework\MockObject\Runtime\PropertyHook;
use PHPUnit\Framework\TestCase;
final class DoubledPropertyMockExampleTest extends TestCase
{
public function testExample(): void
{
$mock = $this->getMockBuilder(ClassWithPropertyWithoutHooks::class)
->doubleProperties(['property'])
->getMock();
$mock
->expects($this->once())
->method(PropertyHook::set('property'))
->with('value');
$mock->property = 'value';
}
}
Best Practices
Favour doubling interfaces over doubling classes.
Use meaningful names: Name your test doubles clearly to indicate their purpose.
Keep it simple: Do not over-configure test doubles. Only configure the methods you need.
Test at the right level: Mock service boundaries (repositories, external services), not domain objects or value objects.
One concept per test: Test either state (with stubs) or behavior (with mocks), not both in the same test.
Avoid brittle tests: Do not mock internal implementation details. Mock interfaces and public contracts.
Review PHPUnit notices: If you see a notice about unused mock objects, consider whether you should add expectations or use a test stub instead.
Sealed Test Doubles
The seal() method is part of the fluent API for configuring test stubs and mock objects.
When you seal a test double, you prevent any further configuration and signal that the test double is fully configured.
Key Benefits
Prevents accidental misconfiguration: Once sealed, any attempt to configure additional behavior or expectations will error the test
Stricter mock behavior: For mock objects, sealing adds implicit
never()expectations for all methods that were not explicitly configured, causing the test to fail if those methods are calledSelf-documenting tests: Sealing explicitly communicates that the test double configuration is complete
Sealed Test Stubs
This example shows how a test stub is sealed using the seal() method:
public function testDemonstratingSealedTestStubs(): void
{
$stub = $this->createStub(InterfaceName::class);
$stub
->method('doSomething')
->willReturn('value')
->seal();
// ...
$stub->doSomething();
$stub->doSomethingElse();
}
A sealed test stub’s method that has not been configured, such as doSomethingElse() in the example above, can be called.
Unless the return value generator is disabled, a sealed test stub’s method for which no behaviour has been configured (returning a value or throwing an exception) will return the minimum viable value compatible with the method’s return type declaration.
Sealed Mock Objects
This example shows how a mock object is sealed using the seal() method:
public function testDemonstratingSealedMockObjects(): void
{
$mock = $this->createMock(InterfaceName::class);
$mock
->expects($this->once())
->method('doSomething')
->seal();
// ...
$mock->doSomething();
$mock->doSomethingElse();
}
A sealed mock object’s method that has not been configured, such as doSomethingElse() in the above example, is not expected to be called.
If it is called, then the test will fail. This is equivalent to the following:
public function testDemonstratingSealedMockObjects(): void
{
$mock = $this->createMock(InterfaceName::class);
$mock
->expects($this->once())
->method('doSomething');
$mock
->expects($this->never())
->method('doSomethingElse');
// ...
$mock->doSomething();
$mock->doSomethingElse();
}
Requiring sealed mock objects
The seal() method explained above provides developers with an opt-in mechanism to finalise the configuration of mock objects and prevent unexpected method calls.
However, individual developers must remember to call this method on their mock objects for this safety feature to take effect.
As projects mature and teams establish testing standards, it becomes valuable to enforce certain testing practices at the project level, rather than relying on individual developers to apply them consistently.
This is why requireSealedMockObjects="true" can be used in PHPUnit’s XML configuration file to set up a project-level policy to enforce the best practice of sealing mock objects.