Automatic injection of mocks into test subjects via #InjectMocks and #Mock annotations, to speed up unit testing with PHPUnit.
Leia a versão em português aqui.
To install in the development environment:
composer require --dev silasyudi/inject-mocks
Using #InjectMocks and #Mock annotations in test classes you can automatically inject mocks into test subjects.
In a typical scenario, we would do it like this:
class SomeTest extends PHPUnitFrameworkTestCase
{
public void testSomething()
{
$someDependency = $this->createMock(Dependency::class);
$anotherDependency = $this->createMock(AnotherDependency::class);
...
$subject = new Service($someDependency, $anotherDependency, ...);
...
}
...
This approach brings the difficulty of maintenance, because if the test subject is changed, either by adding, decreasing or replacing the dependencies, you will have to change it in each test.
With the #InjectMocks/#Mock annotations, we abstract these test subject changes. Example:
use SilasYudiInjectMocksInjectMocks;
use SilasYudiInjectMocksMock;
use SilasYudiInjectMocksMockInjector;
class SomeTest extends PHPUnitFrameworkTestCase
{
#[Mock]
private Dependency $someDependency;
#[Mock]
private AnotherDependency $anotherDependency;
...
#[InjectMocks]
public function setUp() : void
{
MockInjector::inject($this);
}
public void testSomething()
{
// $this->subject e as dependências já estão instanciadas.
}
...
As in the example in the previous topic, the #InjectMocks attribute must be placed on the property of the test subject that you want to test, and the #Mock attribute must be placed on the properties corresponding to the dependencies that you want to mock or inject.
After that, run the injector service with the sentence MockInjector::inject($this)
. This execution can be declared in
each test or in setUp
.
After executing the injector, the service
annotated with #InjectMocks will be a real instance available in the scope
of the test class, and each dependency annotated with #Mock will be an instance of MockObject, injected into the test
subject via the constructor, and will also be available in the scope of the test class.
MockInjectException
exception.#InjectMocks and #Mock work independently and alone, or together. Details about each one:
It will create a real instance through the constructor, and if there are parameters in the constructor, the following value will be used in each parameter, in this order:
mock
created from the #[Mock] attribute, if one exists;default
value if it is an optional parameter;null
if it is typed as null
;mock
if it is not a primitive type. In this case, this mock
will not be injected in the TestCase
scope;MockInjectException
exception.Obs.: You can use #Mock attribute on all, some or none of the test subject's dependencies.
Will create a mock
injected into the TestCase scope, without using the constructor. This creation behavior is
identical to TestCase::createMock()
.