
This is not a guide to what characters you need to enter in the code editor to get unit tests. This is the food for the mind that needs to be consumed
before taking the mentioned actions.
The topic of unit testing is not as simple as it may seem. Many of us, developers, come to unit testing under pressure from customers, employees, colleagues, their idols, and so on. We quickly understand its value, and, having completed the technical preparations, we forget about the big picture, if we ever understood it at all. In this article, I will briefly discuss what unit testing is and is not in general, as well as in PHP, and at the same time I will describe what unit testing takes place in QA.
What is testing?
Before delving into the unit tests, you need to study the theory of testing itself, so as not to make mistakes like the one that the authors of one of the most popular PHP frameworks did: they showed integration tests on their website and
called them modular. No, Laravel, these are not unit tests. Although this does not prevent me from still loving this framework.
Software testing is
defined as “an investigation conducted to provide information about product quality to interested parties.” This is opposed to "testing software - it is a waste of the project budget by developers who do not do anything important, and then ask for more time and money, because" nothing "can be very expensive." There is nothing new here.
Here is my brief history of becoming a test:
- 1822 - Difference engine (Charles Babbage).
- 1843 - Analytical engine (Ada Lovelace).
- 1878 - Edison introduces the term "bug."
- 1957 - Testing and debugging programs (Charles Baker).
- 1958 - First Software Testing Team (Gerald Weinberg).
- 1968 - The PO Crisis (Friedrich Bauer).
- 1970s - Waterfall Model, Relational Model, Decomposition, Critical Analysis ( Walkthrough ), Code Design and Inspection, Quality and Metrics, Design Patterns.
- 1980s - CRUD-analysis, system architecture, auto-testing, V-model, reliability, cost of quality, methods of use, OOP design patterns.
- 1990s - Scrum, usability testing, MoSCoW, heuristic testing, software automation and testing.
If you belong to the millenial generation, like me, you may be amazed that the teams of testers existed long before you were born. Stop for a minute, inhale, exhale, calm down.
The story shows how over time the type of testing changed, which was considered “good enough” for interested parties. Approximate phases, on what were oriented when testing:
- ... - 1956 debugging
- 1957 - 1978 demonstration
- 1979 - 1982 destruction
- 1983 - 1987 assessment
- 1988 - ... prevention
Therefore, unit testing is necessary to
prevent inconsistencies between the project and the implementation.
What is testing really?
There are different classifications of software testing. In order to better understand the place of unit testing, I will mention only the most widespread approaches.
Tests are: static and dynamic, "box" (white box, black box, gray box), levels and types. Within each approach, different classification criteria are used.
Static and dynamic testing
Static testing is performed without executing the code. This includes proofreading, checking, code revision (while observing the work of another / pair programming), critical analysis, inspections, and so on.
Dynamic testing to get correct results requires executing code. For example, for
unit tests , integration, system, acceptance and other tests. That is, testing is conducted using dynamic data, input and output.
"Box" approach
According to this approach, all software tests are divided into three types of boxes:
- Testing of the “ white box ” type checks internal structures and modules, ignores the expected functionality for end users. This could be API testing, fault injection, unit testing , integration testing.
- Testing of the “ black box ” type is more interested in what the software does and not how it does. This means that testers are not obliged either to understand the object of testing, nor to understand how it works under the hood. This type of testing is aimed at end users, their experience of interacting with the visible interface. “Black boxes” include model-based testing, testing of usage methods, state transition tables, specification testing, etc.
- Testing of the “ gray box ” type is designed with knowledge of software algorithms and data structures (white box), but is performed at the user level (black box). This includes regression testing and pattern testing.
Now, in order to confuse you, I will say that
unit testing can also refer to the “black box”, since you can understand the module under test, but not the entire system. Although for me it is still a “white box”, and I suggest you agree with this.
Testing levels
Their number varies, usually in the range from 4 to 6, and all of them are useful. Names can also be different, depending on the culture adopted by the company, you may know “integration” tests as “functional”, “system” tests as “automated”, and so on. For simplicity, I will describe 5 levels:
- Unit testing
- Integration testing.
- Testing component interfaces.
- System testing
- Operational acceptance testing.
Unit testing checks the functionality of a specific piece of code, usually one function at a time. Integration testing tests the interfaces between components so that the modules assembled together form a system that works as intended. This is an important point, because a large number of tests, which are called modular, are actually integration tests, and the developers consider them as modules. If the use of multiple modules is implied, this is testing the integration between them, and not the modules themselves. Testing the interfaces of the components checks the data transmitted between different modules. For example, received data from module 1 - checked - transferred to module 2 - checked. System testing is end-to-end testing to verify compliance with all requirements. Operational acceptance testing is performed to verify readiness for operation. It is not functional, only the serviceability of the services is checked, whether any subsystems damage the environment and other services.
Types of testing
Each type of testing, regardless of its level, can also be divided into other types. There are more than 20 common types. The most common:
- Regression testing .
- Acceptance testing.
- Smoke (smoke) testing.
- Uat
- Destructive testing .
- Performance Testing
- Continuous testing .
- Usability testing
- Security testing
From the name it is clear what this or that type of testing is intended for. Bold highlighted unit tests in PHP. If you really want, then each of these terms can be applied to unit testing. However, the main type of unit tests are regression tests, which check whether all modules of the system are executed correctly after making changes to the code.
Now you know that modular tests are dynamic, belong to the “white box” class, are performed at the module level, are regression tests, but at the same time many types of tests can be understood as modular tests. So what exactly are unit tests?
What is unit testing?
V-model is a graphical representation of the above levels, types and their purpose in the software development life cycle.

After checking and approving detailed product requirements, when we started writing code, the unit tests are the first line of defense against any inconsistencies. Therefore, companies that understand what they are doing force developers to use unit tests or even TDD, since it is much cheaper to fix bugs in the initial stages than in later ones.
And this is true. Unit tests have a lot of advantages. They:
- Isolate each part of the program and check its correctness.
- Help to detect problems early.
- They force developers to think in terms of input, output and error conditions.
- They give the code a convenient form for testing, facilitate future refactoring.
- Simplify the integration of working (!) Modules.
- Partially replace technical documentation.
- Forcing to separate the interface from the implementation.
- Prove that the module code works as expected (at least mathematically).
- Can be used as low-level regression test sets.
- Demonstrate progress in incomplete system integration.
- Reduce the cost of fixing bugs (with TDD - even more).
- Allows you to improve the application architecture by defining the responsibility of modules.
- If you can test it, you can connect it to your system.
- Unit testing is FUN!
However, there are certain limitations that you thought of, probably while reading this list:
- Unit testing does not catch integration errors.
- Each boolean expression requires at least two tests, and the number is growing rapidly.
- Unit tests are as buggy as the code they are testing.
- Linking tests to a pair of specific frameworks or libraries can limit the workflow.
- Most tests are written after development is complete. Sadly Use TDD!
- Perhaps, after a little refactoring, the system will work as before, but the tests will fail.
- Rising cost of development.
- Human error: commenting on broken tests.
- Human error: adding workarounds to code specifically for passing unit tests.
The latter is killing me the most. (Almost) in every project, right in the source code of the working application, I find lines like “if this is a unit test, load a surrogate SQLite database, otherwise load another DB”, or “if this is a unit test, do not send an email, otherwise send "and so on. If your application has a bad architecture, do not pretend that you can fix the lousy software with good test passing, it won’t get any better.
I often discussed with colleagues and clients what a good unit test is. Is he:
- Fast.
- Automated.
- Fully manages all your dependencies.
- Reliable: can run in any order, regardless of other tests.
- It can be run only in memory (no interactions with the database, readings / records in the file system).
- Always returns one result.
- Convenient to read and maintain.
- Does not test the SUT-configuration (system under test).
- It has a clearly defined ONLY TASK.
- Well-named (and understandable enough to avoid debugging just for the sake of figuring out what's wrong).
To those who grinned after reading “automated”: I did not mean integrating PHPUnit or JUnit into CI pipelines. The point is that if you change the code, save it and do not know whether the modules pass their tests, they are not automated, but should. The winning option is file tracking (File watcher).
What should be subjected to unit testing?
In normal systems, unit tests need to be written for:
- Modules are indivisible isolated parts of a system that perform any one task (function, method, class).
- Public methods.
- Protected methods, but only in rare cases and when no one sees.
- Bugs and fixes.
The definition of a unit test depends on the developer who wrote the code. In PHP, this is almost always a class method or function, because it is an
indivisible piece of software that makes sense by itself . Several times I saw how the developers used an array of single-method mini-classes as one module. This makes sense if the minimum functionality requires multiple objects.
So you can determine for yourself what is a module for you. Or you can test the methods one by one, simplifying the life of that guy, who will then work with the code.
If you are not conducting a unit test, I suggest doing this after the next big bug occurs. Check what method it will be associated with, write a failed test with the correct arguments and result, fix the bug, run the unit test again. If it is passed, you can be sure that this bug had to be fixed for the last time (taking into account your specific input scripts).
This approach makes it easier to understand unit testing. Analyze each method separately. Data providers can help determine input and output data for any scenarios that may occur to you, so whatever happens you will know what to expect.
What NOT to test
Slightly more difficult to determine what testing is not necessary. I tried to collect a list of items that
do not need to
be subjected to unit testing:
- Functionality outside the scope of modules (!)
- Module integration with other modules (!)
- Uninsulated behavior (unmockable dependencies, real databases, network)
- Private, protected methods.
- Static methods.
- External libraries.
- Your framework
I am sure that you should not apply unit testing to any of the above, except for static methods. I like to argue that static, in essence, means procedural, and in many cases, global. If a static method calls another static method, then this dependency cannot be overridden. This means that you are not testing now in isolation. And then this is not modular testing. On the other hand, this is the part of the code that can live on its own, it has a purpose, and it needs to be tested to make sure that whatever part of this confused system the test part of the code causes, it will not break. Therefore, I believe that it is possible to test static methods if you are sure that the output of your test cannot be changed by any other test, and that the language or framework will allow you to test natively.
How to write unit tests?
- Write code suitable for unit testing, then test it.
- Write code suitable for unit testing, then test it.
- Write code suitable for unit testing, then test it.
If “then test it” is not enough, then there are some very good PHP unit testing videos on
laracasts.com . There are lots of sites dedicated to the same task in other languages. I see no reason to explain how I perform unit testing, because the tools change quite quickly, and when you read this text, I can switch from PHPUnit to Kahlan. Or not. Who knows.
But answering the first question (how to write code suitable for unit testing) is much easier, and the situation is unlikely to change much over time:
- SOLID
- DRY
- Absence of new keywords in the constructor.
- The absence of cycles in the constructor (and transitions, if this is stipulated).
- Lack of static methods, parameters, classes.
- Lack of setup () methods: objects must be fully initialized after construction.
- No singleton (global state) and other untestable antipatterns.
- The absence of omnipotent objects (God objects).
- The lack of classes with mixed functionality (mixed concern classes).
- No hidden dependencies.
Now, knowing what unit tests are and are not, what is needed and what is not needed to be tested, what place the module tests take in the software development life cycle, it will be easier for you to implement them. It remains to find a framework or library for the soul. If in doubt, take the framework / language that has become the de facto standard.
In conclusion: unit tests are very important both for developers and for business. They need to be written, there are proven techniques that will help you easily cover the modules with tests, mainly by preparing the modules themselves. But all these techniques do not make sense without knowledge of the theory of testing described in this article. You need to be able to distinguish unit tests from other types of tests. And when you have a clear understanding in your head, then it will become much easier for you to write tests.