In this article, I would like to show you how to test a PHP application. I will go over PHP unit testing as well as many other types of tests. There will be many PHP-specific tools and best practices for you to adapt. So, all of you PHP enjoyers out there, dig in!
“Don’t waste your time for tests when you’ve got a client with a mouse and keyboard.“ – said no one, ever (I sure hope so!).
Do you really need to create tests? Of course, there are many reasons to do so – improved quality of software, decreased risks while making changes in the code, identifying errors, checking business requirements, improving security…I could go on and on with that. The point is – tests do make a difference.
Unit tests in PHP🧪
The most popular type of tests that programmers write. In these, you should focus on single units in your code. The unit can be a single class or a function – a small piece of code that does a specific thing. Unit tests can be used for easy and fast checking business logic in the code. Every test should check one path – a possible way of executing the function. It’s recommended to cover in your tests as many different paths as possible (and that applies not only to the unit tests but to all kinds of tests). The unit test doesn’t need to have the whole application stack running – you don’t need a connection to the database or anything while running them.
I’m going to start by showing what a simple unit test looks like. As an example, I will test a simple Product class that consists of name and quantity. I can increase the quantity of a product with a dedicated function for that (increaseQuantity):
How to test it? In PHP, the most common library which helps you to do that is PHPUnit. You can read about how to install and run it here. The simplest test which checks our Product class can look like this:
The test is checking whether the increaseQuantity function will properly change the amount of product. To check the result, I am using assertions. In this case, I am comparing the expected result with the actual quantity of a product.
I also divided our test into three parts – Given, When, and Then. These terms are taken from Behavior-Driven Development (I will move on to that later). However, it’s also applicable to unit tests. Unit tests even have their own pattern name for that, which is called AAA (Arrange-Act-Assert), but the idea is the same.
- The first part is preparing data for the test.
- In the When/Act section I am writing the actual invocation of a tested method.
- Finally, I am making assertions to check if expectations were met.
The tests look much more readable and self-explanatory. Thanks to that, you see what is being tested here at the first glance. I highly recommend you to start writing unit tests in this way if you are not doing that yet.
Don’t forget about other, unhappy paths of executing our code. When writing your tests, think about edge cases – for example, wrong input data. What if I pass negative numbers to increase the quantity of a product? The answer is to write a test for that:
Of course, the code is typically not as simple as in the example provided above. Sometimes it’s very complicated and has many dependencies. It’s hard to prepare such a code for a unit test.
The class below is representing an order in a shop, which you can add products to:
If you do not want to test every dependency in tests, you can use Test Doubles. It’s a term for any kind of pretend object used in place of a real object for testing purposes. They can control the flow of executing code.
You can steer how the dependencies behave or even what they return. The most popular are mocks:
In the example above I mocked the shop which has products. I want to be sure that the shop will remove a product after I add it to the order. So I wrote a mock which is expecting that. Now I can check if adding a product is working correctly.
Personally, I am not a big fan of mocks. Of course, they can be really helpful while testing some big parts of code or to test if a logger has logged some information. However, steering your tests and as a result, your code, is risky. What’s more, mocks are violating the AAA rule. Notice that you need to write your expectations (asserts) before the actual Act part.
Mocks can lead to some fake unit tests by mocking only happy paths. For example, mocking the results of some complex database queries can have this effect.
One of the options for replacing mocks is using spies. These allow you to verify that a method was called instead of setting an expectation in advance that it should be called. And you cannot steer the output of these functions.
See how the previous test looks with a mock replaced by the spy (I’ll use for that additional Mockery object framework):
Test Doubles leads to focusing on testing the implementation. And it should not be the purpose of unit tests. Use them wisely, not as an easy solution for everything.
A great supplement for them is integration tests.
Integration tests 🖥 ↔ 🖥
Integration tests are a great addition to unit tests. Integration tests are more focused on connections between the modules and are on a higher level than unit tests. They are testing the integration between application components (like modules, classes, services) and can be dependent on some configurations, such as database connection parameters. They are not as fast as unit tests but can do more – testing your database queries, integrations with some other services, etc.
For integration testing of PHP applications, you can also use PHPUnit. The previous OrderTest without mocks as an integration test looks like this:
Acceptance tests 🤝
Their task is a little bit different from the previously mentioned tests. The main role of acceptance tests is to determine if the requirements of the application are met. There are several types of them, including user acceptance testing where the end-users are checking the application. I will focus on acceptance tests with the usage of the BDD framework.
Behavior-Driven Development (BDD) is a software development practice focused on writing tests and documentation in natural language. That language is Gherkin, which is created specifically for behavior descriptions (you can read more about it here). It helps specify functionalities with the client. In a perfect world, the tests should be written during gathering requirements.
There are special BDD tools made for PHP – Behat, and PHPSpec. However, it’s also possible to write acceptance tests using PHPUnit. Now you know why I used Given, When, Then notation before in unit tests – it’s nothing else but Gherkin keywords.
Getting back to the first test I wrote in this article – don’t you agree that it should be understandable even for non-technical people?
This is how an example test would look using Behat:
Pretty simple, right? There are no technical or implementation details, just pure business logic. But how does PHP check that and know what is it all about? Take a look behind the scenes:
Behind every sentence in the Behat scenarios, there is a hidden piece of code. In Context classes, I can create functions. Behat does all the magic and matches them via annotations with a pattern.
Behat is a powerful tool for testing our application. You can write documentation with a natural language and tests at the same time! It’s also good to know that Behat can be used as a tool for testing compliance of the REST API with the contract.
Mutation tests 🧬
Mutation tests are strictly related to previously mentioned unit tests. They can improve the quality of unit tests by introducing random changes in the source code that should cause the test unit to fail. My colleague has made a practical tutorial of mutation testing in PHP with examples. I highly recommend you give it a try.
Static code analysis 🤓
Although it’s not any kind of test, it can help during the process of verifying your application. Static code analysis is reading the source code without running it. This allows you to predict the behavior of code before it executes. There are many types of code analyzers for PHP and the most popular are PHPStan, EasyCodingStandard, PHP Mess Detector. If you want to learn more about them you can check my colleague’s article about static code analysis.
Architecture testing 🏛
Architecture tests belong to static code analysis. However, it is worth mentioning a little more about them. These are very helpful when you want to keep your architecture clean and don’t want to have any leaks in the code between layers. You can set what each layer can use and what it can be dependent on. In PHP, the examples of such tools are Deptrac and PHP Architecture Tester. You can see how to install it on its GitHub pages (qossmic/deptrac and carlosas/phpat). I’m going now to show you how architecture tests work in practice by using the deptrac tool.
I have a three-layered architecture in my project: Domain, Application, and Infrastructure. I also added one common layer called Utils, which contains some tools like my own Collection implementation or DateTimeProvider. According to good practices in Domain-Driven Design I can extract few rules:
- Domain should not depend on any other layer (however there is a common layer Utils so you can allow for using it; it applies to other layers as well).
- The application can depend only on Domain.
- Infrastructure can depend on Domain, Application, or any other packages
In Deptrac, you start by defining your depfile in which you can describe all rules mentioned above. You need to define every layer (the most common way is by assigning proper namespace regex to each layer) and then make a ruleset. The example file should look like this:
After running it, you should receive an output:
Let’s see what happens when I will use the database repository in my domain layer:
As you can see, deptrac did not allow me to use a repository from the infrastructure layer in the domain. This tool can be handy when you want to write framework-agnostic applications.
Architecture tests take a burden from you and guard the boundaries between the layers in our code. They help you to write better code.
Manual testing 👷
It’s important not to forget about the manual testing of your application. I have some tips for you that I use myself during the development process:
- Read functional requirements once again after creating new functionality. Check if it meets them and it does what it should.
- Make manual tests of new functionalities before pushing your code to code review. Don’t trust your unit and acceptance tests straight away.
- Don’t focus only on the happy path. Try to execute every possible path.
- If your application is exposing API, use Postman to test it. Save every path as a separate request and save it as Postman Collection.
- The same applies to some console commands. Try to break your commands and see what happens on the wrong input.
Performance testing 🏃
Sometimes, your application must accept many concurrent requests or take a load of a few thousand requests per second. How can you check if the application meets all of those requirements? The performance tests to the rescue!
If you want to learn more about it, my colleague from the QA team wrote a really comprehensive k6 framework article.
PHP unit testing and other types of tests – how much should you test?
As you can see, there are many ways to test your PHP applications. What should be their number? Which should you use? There are no simple answers to these questions. There is a graphic representation that describes the number of tests you should have in each group – it’s called the Test Pyramid.
As you can see, unit tests are the base of the pyramid. These should be the most common. They are also the quickest ones. The higher the tests in the pyramid, the more dependencies they have. At the top there are E2E tests – these are focused on the end user’s experience and are simulating the real user scenario.
Every kind of test focuses on different things as every project has different objectives. I recommend combining all of them in an automated way via the Continuous Integration process.