This article is not a theoretical guide to writing tests and not how-to on using tools in a certain stack, but a series of popular questions, sometimes even many of which have not been formed, to which I will try to give answers. The source of these questions is colleagues, people from both sides in the interviews and acquaintances, and the answers will be subjective, short and not exhaustive, based on other people's materials and their experience. The target audience of the article is developers who write with certain success, or at least try to write tests, but experience certain difficulties in writing them.
I tried not to be attached to a specific language in order to increase the reach of readers, but I will make a reservation right away that I work in the PHP ecosystem using PHPUnit, and therefore some of my conclusions may not be suitable for other ecosystems. When selecting questions and writing, I focused on many reports and articles and used them as a reference.
The reason for writing was the recent article “ PHPUnit. Wretting the Doctrine Entity Manager ”from trawl , some of which I’ll also discuss.
A list of questions:
- To write or not to write tests?
- And if time is not allocated for tests?
- Types of testing, how to choose?
- Why is it difficult for me to write tests for a long time?
- How to test private methods?
- How to write integration tests? How to test the base?
- How to: integration or functional?
- What to do with external dependencies?
- How to simplify navigation between tests and test subject?
- Should I use TDD?
- What else can be used to improve the code?
To write or not to write tests?
I saw a lot of opinions on this subject, but I myself came to my own: we must focus on the needs of the business.
For a piece of code done in a couple of hours on the knee, tests are not needed. On the other hand, for a large corporate project for hundreds of manpower, all popular types of tests are required. And everything that is between these poles should be considered as a special case, evaluating the cost of certain types of testing: with tests, it should be less than without them. Personally, I write smoke tests even for a tiny CRUD project lasting a couple of weeks, because already at such a distance they bring benefits and reduce the development cost.
Advantages of testing, briefly and abstractly:
- A significant reduction in the cost of fixing the bug due to early detection.
- Fixing contracts.
- Low level documentation.
- Detection of architectural problems.
So if your projects are not one-time scripts from several files, then I definitely recommend writing tests. Therefore, the question from the title should be reformulated: “To what extent should tests be written, and which ones?” More about this later.
And if time is not allocated for tests?
This is a very controversial issue, so I will make a reservation once again that I am talking exclusively about my subjective opinion, while I try to do this as delicately as possible.
Writing tests is just as much of a task as thinking through, coding, or debugging it. Tests are the same code as business logic or controllers. But do you have any questions about them? You do not set a separate task for writing a controller and do not coordinate it with managers?
It is not necessary to devote additional time to writing unit and integration tests. Tests are written as part of the main task. The technical leader of the project, as the owner of the project repository and a competent person, decides how, what and with what kind of coverage to write tests, focusing on the tasks of the business, not inflating, but reducing the cost of development.
However, it may happen that the customer does not believe in the tests and believes that with their help you do not save, but only waste time. He pays money and sets conditions, with the same success he can forbid you to use a dark theme in your IDE or order to use five spaces as an indent. And if the conditions are not discussed or the discussion is at an impasse, then your choice is to accept such conditions or refuse. With advising consequences.
I will make a reservation that the latter is true only if you are able to write tests, adequately assess the costs of them and the estimated payback. If you want to learn how to write them at the expense of the employer on a short-term project, and he is against it, then I am on his side.
Types of testing, how to choose?
You can look at the testing pyramid of Mike Cohen: as developers, we are interested in the two lowest levels of testing. The maximum effort should be given to unit testing, it is the cheapest (by cheapness I mean the time spent on development and support), such tests are very simple to implement, very simple to run, they work quickly.
But is unit testing always applicable or practical? I believe that for primitive CRUD such tests will take too much time with little impact and do not guarantee anything. Try to test the repository (Repository in DataMapper) and then answer the question, what did it give us. But for a variety of calculators, this approach will be ideal.
Always try to write unit tests wherever possible, but do not test what is useless to test.
How to test the joint work of the backend and frontend, if these are different projects on different stacks? Just like the backend and the mobile application: this is system testing, and it should be of interest to QA and DevOps engineers, not developers (well, only if you do not have a real merciless scrum, where the only available role is a full stack developer).
In addition, system testing is the most expensive in terms of development and support, both in time and in infrastructure. The solution of issues related to this already lies outside the competence of “linear” developers, and the question of its application and volumes should be decided by the technical director and leaders of the areas together with the business.
Why is it difficult for me to write tests for a long time?
Sometimes it’s difficult to write tests because the code is not ready for them. A pile of mobs, where some of them give away others, are also a consequence.
Do not use registries, singletones, locators, you greatly complicate your life with this. This was the main claim to the article to which I referred above. Use DI, and implement the necessary repositories in the services right away. Observe the law of Demeter to avoid chains of mokas. Try a little work using the TDD methodology.
Treat the tests like first-class code, observing the same high quality code as in business logic. Poor test code quality will lower productivity over time.
How to test private methods?
No way. We are testing a contract - the interface provided by the module to other modules. We do not need to test the internal implementation, it can change.
What if there is some complicated logic inside the test, and testing the contract turns into a large amount of input, where something strange happens inside? It is necessary to refactor the code, distributing it into different classes, where such private methods will become public, they already write unit tests on them. And if there were no tests, such places would be difficult to detect, which would increase the cost of supporting such code.
How to write integration tests? How to test the base?
Use fixtures. Explore popular tools on your stack.
Do not write large universal packs of fixtures, use their minimum number. If you need a certain state of objects for your tests, then do not reuse such fixtures in other places where such a state is not required, otherwise the test support will be complicated: changing them for some tests will break others and require additional time.
There are integration tests that do not go through a complete cycle from request to response, but the interaction of classes is checked only inside the component. If they do not have direct work with the database, then you can make a set of data and / or mok that will go into the input. In such cases, you can avoid the use of fixtures and reduce the complexity and execution time of such integration tests to the modular level.
How to: integration or functional?
Integration testing is one of the testing levels in terms of isolation. Functional testing - compliance testing. These definitions lie on different planes, and you can’t put an equal sign between them, but in practice in conversations between developers they mean the same thing, although this is not correct.
What to do with external dependencies?
We replace external dependencies with moki. Not only for unit tests, but also for integration tests.
For example, we use the HTTPS client to access some API through the Guzzle class. If you create an instance of such a class inside the class under test, it will be difficult to replace it, but the solution will be very simple: we implement such a client in the constructor, and during testing we will replace it with a moch.
How to simplify navigation between tests and test subject?
Modern development tools can track the location of tests or test classes if you use naming standards. For ease of navigation, you can use the keyboard shortcut Ctrl + Shift + T in JetBrains products, in addition, if the test does not exist, you will be prompted to create it and make a wireframe.
Sometimes you need several different test classes or methods for the subject of testing, in this case you need to help the IDE, for example, add the @covers annotation in the case of PHPUnit.
Should I use TDD?
TDD is one of the development methodologies in which iteratively and necessarily write tests first, and then code. Trying it is definitely worth it, it will teach you how to write code that is well tested. However, such an approach was not widely used, it has problems. One of these problems is that you write tests that would not write with the classic approach. And such tests also need to be supported, spending time on it, and the benefit from them is small. For example, tests for setters or tests for classes where nothing happens except dependency calls. Such places are best checked by integration testing.
What else can be used to improve the code?
Static analysis tools are another way to improve the quality of your code. They partially overlap with the tests by the result, but they often find that the tests were not found, especially with poor coverage. Their undoubted advantage - you see the result immediately in the IDE and get feedback instantly.
This does not mean that it’s enough to leave them only on the IDE, I highly recommend using them on the CI to see potential errors and not to spoil such code.
I also recommend adding code style checking tool support to the IDE and CI. Errors are often hidden among carelessly written code, and such tools allow you to find such areas.
Materials for further study