Recently at SCAYLE, I wrote a Laravel service that inserts data using Eloquent into a table like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | class HelloWorldRepository { public function create( int $helloWorldId, string $description, string $createTs ): HelloWorld { $helloWorld = new HelloWorld(); $helloWorld->hello_world_id = $helloWorldId; $helloWorld->description = $description; $helloWorld->create_ts = $createTs; $helloWorld->save(); return $helloWorld; } } |
For people not familiar with Laravel/Eloquent: The HelloWorld
model mirrors the database structure and the save
method persists data into the database table.
So far, so good. Like for any good and responsible project, I wrote unit and integration tests for the HelloWorldRepository
and decided to mock the HelloWorldRepository
class using Mockery
. Mockery
lets the mocked instances “act like” they are the actual class and have a pre defined behaviour. And this is what I want to address in this blog post.
What is mocking?
According to the GitHub page, Mockery is defines itself as follows:
Mockery is a simple yet flexible PHP mock object framework for use in unit testing with PHPUnit, PHPSpec or any other testing framework. Its core goal is to offer a test double framework with a succinct API capable of clearly defining all possible object operations and interactions using a human readable Domain Specific Language (DSL). Designed as a drop in alternative to PHPUnit’s phpunit-mock-objects library, Mockery is easy to integrate with PHPUnit and can operate alongside phpunit-mock-objects without the World ending.
So basically, the created instances are doubles of the service. Mocking library dig deep into PHP core, create and design objects at runtime using reflection and replace the classes. I want to outline this point: Mocking does not mean that there is an alternative implementation of an instance or so. The mocked instance is a double, created at runtime using a lot of reflection magic and a pre defined behaviour defined by the developer.
What is critical about mocking?
From my experience with mocking, the critical thing is the pre defined behaviour as mentioned above. Most of the time we have a very clear, linear idea of how code should behave. While in general this is a bad thing, however, for simple cases (like inserting into a database), it can be enough. And so did I:
1 2 3 4 5 | $helloWorldRepository = Mockery::mock(HelloWorldRepository::class); $helloWorldRepository ->shouldReceive('create') ->with(99, 'Hello Ucar Solutions', '2022-10-31 20:55:00'); $this->instance(HelloWorldRepository::class, $helloWorldRepository); |
While the code above is correct and the tests run successfully, there was one major problem what is not obvious at first glance.
The HelloWorldRepository
abstracts the layer below – the Eloquent model – away and ‘acts like’ the real code. In this case, the abstraction was fatal since the hello_world
database table did have an id
field as the primary key, but no auto_increment
defined. As we do not touch Eloquent, but tell our test double to ‘act like’ we do, the mocked instance returned the expected instance and everything was fine. But in production, the real code failed with missing Field 'id' doesn't have a default value
.
And this is the point: we define a behaviour and test exact this behaviour in our unit/integration test! This makes the test somehow obsolete since we know the result beforehand.
Should I stop using mocks?
Depends. If you create many expectations, test all them in different test cases and use mocked instances in a very low level of your project architecture, then, mocks can make sense. As a rule of thumb: the higher the level of the mocked class in your architecture, the untrustable the test gets.
What do I mean? – Let’s imagine an API endpoint that returns data from a third-party service. If you mock the entire API endpoint request handler class, then, well, the test seems to be useless. If you just mock the http request layer and let your request handler “work as coded”, then it makes much more sense.
A Better alternative: Stateless Services and Dependency Injection
If the project allows, I avoid using mocking and mocked classes because of the reasons outlined above. In this specific case with the Laravel project, it was not so easy to do so since Laravel depends and provides mocking (and magic at all). From my perspective, the better alternative is to follow a strict stateless service architecture with Dependency Injection.
Why stateless? Because stateless classes are easier to test (and easier to mock, btw.) and very easy to replace. For instance, a service that returns data from a third-party service can just return a JSON string instead of doing the real http call. This service can be created in the test namespace and is only used for tests. Of course, the real services should all implement an individual interface that can be used as a blueprint for the test services and as the registration key in the DI container.
On the other hand, Dependency Injection allows the actual replacement of the stateless service. You do not have to interfere with the natural flow of the code – just create the test as explained above and overwrite the entry in the DI container.
The advantage of this approach is obvious: the service does not make expectations, does not ‘act like’ a class and is just part of the (test) application. There is no mocking library in between and no ‘just in time’ created instances using reflection.
Let’s take a real example from the open source project Keestash: Keestash has an event manager that dispatches events from different parts of the code and listeners can register themself in order to react to the events. Keestash serializes them into the database and a worker daemon retrieves the events and executes all listeners. This approach is also known as making the process (the event management) asynchronous. The corresponding code is:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | .... use KSP\Core\Service\Event\IEventService; .... class EventService implements IEventService { .... public function execute(IEvent $event): void { $listeners = $this->listeners[get_class($event)]; $serializer = new PhpSerialize(); foreach ($listeners as $listener) { if (false === is_string($listener)) { continue; } $message = new EventMessage(); $message->setId((string) Uuid::uuid4()); $message->setPayload( [ 'listener' => $listener , 'event' => [ 'serialized' => $serializer->serialize($event) , 'name' => get_class($event) ] ] ); $message->setReservedTs(new DateTimeImmutable()); $message->setAttempts(0); $message->setPriority(1); $message->setCreateTs(new DateTimeImmutable()); $stamp = new Stamp(); $stamp->setCreateTs(new DateTimeImmutable()); $stamp->setName($listener); $stamp->setValue((string) Uuid::uuid4()); $message->addStamp($stamp); $this->queueRepository->insert($message); } } .... } |
As you can see, the event class is getting serialized and stored – among other relevant data – into a database table. Further, the event class implements the IEventService
interface that is used to register the service into the DI container.
During tests, Keestash does not have a database that can handle serialized classes properly. Further, we would need to run a daemon every time we execute tests which is not what we want. Therefore we needed a solution that executes all events but does not utilize databases. Thus, we decided to run events synchronously instead of serializing into a database. During tests this is acceptable since we do not have time or resource critical infrastructure here. Therefore, the code has been changed to:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | .... use KSP\Core\Service\Event\IEventService; .... class EventService implements IEventService { .... public function execute(IEvent $event): void { $listeners = $this->listeners[get_class($event)] ?? []; foreach ($listeners as $listener) { if (false === is_string($listener)) { continue; } $listenerObject = $this->container->get($listener); if ($listenerObject instanceof IListener) { $listenerObject->execute($event); } } } .... } |
As you see in the code above, the test event class implements the same interface as the real event class. The only difference here is that we retrieve the listener class from the DI container and execute it directly instead of pushing to a database and thus to a worker.
As mentioned above, the interface FQN is used to register the event to the DI container. While Keestash bootstraps tests, we retrieve the actual DI container and the test configs (that one with the overriden service classes), override the DI container and run the tests.
Another advantage of replacing services instead of mocking is the hard to read boilerplate code. Sure, many IDE’s (such as PHPStorm), support mocking libraries but the developer experience is a better one if PHP throws exceptions instead of weird, unintuitive and less-good readable reflection exceptions.
Conclusion
The DI approach described above can also be seen as some kind of mocking. While this is partially true, it is not an entire mock since we use disciplines of the programming language (interfaces), Dependency Injection (the service container) and a clear and solid architecture.
On the other hand, mocking enables defining expectations (such as throw an exception for a given input set) easily. But thus results in writing a lot of boilerplate code and I am convinced that defining expectations in the actual test case is much cleaner and comprehensible than juggling with mocks and reflection.
And sure, I could use database testing by Laravel/Eloquent but I hope you get that this is not the point 🙂