Hi folks! I'm going to talk about the way I use to design my unit tests in PHP.
Disclaimer
Let's start with a (my?) definition of unit testing:
The goal of unit tests is to check the correct behavior of each of the public functions, be given a representative set of data.
The important thing is that external calls inside these functions must be tested in dedicated tests and therefore needs to be mocked.
The interest of having these tests is to run them each time you modify the codebase to verify that no regression was introduced.
As a pre-requisite, the code needs to be testing ready in the sense that we need that to allow mocking of the dependencies. In a given function, a dependency can be internal (e.g. another class of the project), or external (e.g. a call to 3rd-party library).
I won't be talking about the way of making the code testable. I know two techniques to achieve that using the dependency injection principle, but maybe there are other alternatives:
- first, you can modify the interface of your function to pass all its dependencies as parameters (or using the constructor or setters),
- or you can use a Dependency Injection Container (aka DIC) which is a common (anti?)pattern in software development (see PHP's PSR11 for further details).
Code architecture
When I start a new web project, I usually split my code in several distinct layers:
- a router (usually Slim router), responsible for processing HTTP requests,
- a controller layer, responsible for data validation and output rendering,
- a business layer, responsible for the business logic,
- a mapper layer (aka DTO),
- a model layer (aka DAO).
Doing so makes the code more testable.
Some additional stuff may be required, depending on the needs, such as:
- checkers, to validate inputs,
- views, to render outputs,
- middlewares, to add pre- and post-processing of HTTP requests,
- routes, to manage the REST API's routes,
- enablers, to manage outgoing requests to 3rd-party APIs (SMS, Email,...).
And finally, a bootstrap file to create all the things.
The classical workflow is:
- a controller gets called on a given function,
- it validates the input using a dedicated checker,
- it calls the business layer with verified input,
- the business layer optionally uses a mapper to interact with the db
- it may also interact with a 3rd-party API through an enabler
- it optionally returns models to the controller layer
- the controller renders the models returned by the business layer using a dedicated view.
When I write unit tests for a given layer, I only test the behavior of the layer's functions and I mock the calls to the functions of the sub-layers. For example, when I write a test for a controller, I mock the call to the business layer, to the checker and to the view. When I write a test for a business object, I mock the call to the mapper, to the enabler and to other business objects.
Example
Let's see what it looks like for a controller with a bit of code:
class UserController
{
/** @var UserBusiness */
public $business;
/** @var UserChecker */
public $checker;
/** @var UserView */
public $view;
public function __construct(
UserBusiness $business,
UserChecker $checker,
UserView $view
) {
$this->business = $business;
$this->checker = $checker;
$this->view = $view;
}
public function getById(string $id): string
{
$this->checker->checkId($id);
$user = $this->business->getById($id);
return $this->view->render($user);
}
}
Here you can see 3 dependencies in the getById
function. I choose to pass these dependencies through the constructor of my UserController
class. Alternatively, I could have used a Container passed to the function (or to the constructor), or I could have passed the deps through the function's parameters. The result would have been the same: I have to mock these 3 deps to test the function.
Using mocks
Thankfully, PHPUnit comes with a great API to work with mocks (see Test Doubles). I won't cover all the features here, but the documentation worth a look.
First, I need to mock the checkId
function of the UserChecker
. As you may guess, it raises an exception if the id
is wrongly formatted.
Then, I mock the getById
function of the UserBusiness
.
And finally, the render
function of the UserView
.
class UserControllerTest extends PHPUnit\Framework\TestCase
{
public function testGetById_Ok()
{
$business = $this->createMock(UserBusiness::class);
$checker = $this->createMock(UserChecker::class);
$view = $this->createMock(UserView::class);
$expectedId = 'id';
$expectedUser = new UserModel($expectedId, 'john', 'doe');
$expectedResult = 'result';
$checker->expects($this->once())
->method('checkId')
->with($expectedId);
$business->expects($this->once())
->method('getById')
->with($expectedId)
->willReturn($expectedUser);
$view->expects($this->once())
->method('render')
->with($expectedUser)
->willReturn($expectedResult);
$controller = new UserController($business, $checker, $view);
$actualResult = $controller->getById($expectedId);
$this->assertEquals($expectedResult, $actualResult);
}
}
The same has to be done for every layers.
Conclusion
Writing exhaustive unit tests can be painful as most of the time, you'll spent more time writing the test than writing the "real" code.
But IMO, there is no acceptable trade-off when it comes to testing your app.
I know that other testing techniques exists (like TDT), but I see them as complementary tests as they're closer to integration tests than to unit tests. But maybe I'm wrong?
Please feel free to give your opinion!
Thanks for reading!
Top comments (12)
Thanks for this post Boris, good job! ๐
I'd like to hear your thoughts about why you consider dependency injection an antipattern?
AFAIK dependency injection as such is nothing more than injecting dependencies into a class using it's public interface instead of creating instances inside the class. Nevertheless a very important practice to use.
Maybe you mean using dependency injection container in code is bad? DI containers shouldn't be used in controllers or other code that exists in the same layer. That's really a bad practice in my opinion.
What you describe as layers i would describe as components in a layer. For example a router would be a component in infrastructure layer. I usually think through three different layers: infrastructure layer (routers, request factories, configuration, dependency injection, middleware), application logic layer (controllers, repositories) and business logic layer (entities, value objects).
Lower level components don't usually make appearences in higher layers. For example DI container or router don't appear in application layer. Or repositories in business logic layer.
I can identify a lot of similar situations i've faced in my personal projects that you present in your testing example. A lot of the time it's very annoying stuff to write those mocks and they even come with a big downside: the test is tightly coupled to the implementation. Currently i don't have to knowledge to address this issue well, but it's quite obvious that this could be handled efficiently with integration tests.
Finally, i really like that you use single purpose libraries in this solution. Frameworks are not always needed. Sometimes they even have serious design issues which make it hard to design a layered architecture.
If you're interested, please check my "application template". I think it's really close to how you describe building applications.
Thank you for the article Boris; loving seeing PHP content here on dev.to. Bonus, I also really like testing! So reading on this topic is always a priority for me.
I look forward to the next article from you.
Thank you so much!
When I wrote my previous post about awesome PHP resources here on DEV, I found out that there was quite none about unit tests. So I wrote mine ๐
First thanks for share knowledge about PHP Unit Tests.
Really like your definition of unit, it is important step to you and your teammates. It is similar only that in some cases we adopt Sociable Tests, mainly in class's that are stateless (encapsulate an logic or map some data) based on fact that Business Objects tests will pass throught them.
Let's keeping talk about test. Really useful for all o/
I didn't know about Sociable Tests. According to Martin Fowler, my tests are Solitary Tests. Maybe I'll give another try to Sociable Tests because it seems much easier.
Do you have any experience or know how to these tests (or the mocking thing) in the zend framework?
Which version of Zend Framework are you using? Zend Framework 3 and Zend Expressive are straightforward to unit test, ZF1 is a little more difficult mainly due to its age but I added a lot of unit tests to a couple of ZF1 applications in the past few years so I can advise.
Hey Alex, I asked a colleague about it and he told me that the project currently uses the 2.6 version. But the thing is that I've never used it before, so I know about phpunit but the way is done is Zend is out of my knowledge. Would appreciate any help :)
There's no ZF 2.6 (at least not for the framework as a whole) the latest version of ZF2 was ZF 2.5.3. Do you have a composer.json in the application - this will show you which version you're using.
It is actually pretty straightforward in Zend Framework 2. ZF2 apps use a modular structure so there's tests for each module. You can just follow the unit testing documentation and adapt to your own application where necessary. Regarding the unit testing documentation, there's a few defects. If you use the tutorial app (the documentation for which is flawless, so it might be a good place to start) then you'll find that the unit testing doesn't work at first. If I recall correctly there's some kind of problem with the sample Bootstrap class in the unit testing documentation, but if you're using Composer just include the composer autoloader e.g. from your module's test directory create a Bootstrap.php file with the following:
The next issue you're likely to run into if following the tutorial app or perhaps in your own application if it was configured the same way (which is quite possible) is the
module_paths
config setting in the file config/application.config.php. If it looks like the below:... you should update it to look like:
At least for unit testing especially with the tutorial app this, combined with the Bootstrap.php change mentioned earlier, will get you to a working phpunit configuration and you can follow standard techniques from that point. Hope this helps and good luck with your unit testing adventure!
Hi Juan, I didn't use Zend since the v1, but I don't see why you couldn't use mocks with it.
Like the fact that you use separate packages instead of a heavy framework! Very instructive thank you.
Thanks!
I'm working on a post showing my PHP Toolkit, including libraries and tools, so keep posted, it may interest you!