Original text can be found in this link.
Testing is something extremely important in all software projects. Tests make it easier to alter projects or include new features, give developers more confidence and help them discover flaws as early as possible.
However, it may not be so simple to find projects and teams that really focus on test development, either for lack of experience or for not giving it the necessary importance. My feeling is that software testing is an underrated topic. How many projects have you ever participated and heard the phrase “Let’s develop the functionality and then do the tests when we have time”? This shows how much some projects still can’t see the value of testing.
In this article, I bring some tips and experiences and try to demystify some concepts about tests, to encourage a reflection on how they can change our work experience.
Test-Driven Development is great, but it’s not the only option
It is very difficult to talk about testing without mentioning Test-Driven Development. The concept of making a test that fails, making the test pass or refactoring the test it is a very widespread technique among software testing enthusiasts. And it’s not for nothing: this technique brings a lot of confidence; due to the fast and continuous feedback we have throughout the development process.
After starting to develop projects using TDD, I find it very difficult not to use this technique in some moments. Going step by step, thinking carefully about exception cases, and receiving a quick response, which indicates whether the code you have just made breaks something already implemented, is an amazing feeling.
However, TDD is not the only solution to implement testing in our projects. Don’t feel pressured to apply this technique to your project because it is the “right way” to test yourself. The most important point is that your project has significant tests, which give confidence and control over the execution of the software. Don’t worry so much if tests are done before, during, or when delivering the new functionality.
Still on Test-Driven Development, I recommend a series of videos by Kent Beck (creator of TDD, Junit, Extreme Programming etc.), Martin Fowler (one of the greatest authors of software literature) and David Heinemeier Hansson (creator of Ruby On Rails). During these discussions, titled “Is TDD Dead?”, several points are questioned and I recommend the content even to developers who already have an opinion formed on the subject.
The definitions are not so clear
Developers love dichotomies: if something is true, then it cannot be false; if something is 1, it cannot be 0. Unfortunately, when we talk about tests, some concepts do not fit so well into these boxes. To start things off, the term “unit test ” may vary, depending on the source of your research. When asked about a definition, Kent Beck replied that during the first morning of his course, 24-unit test definitions are addressed.
Although we have several definitions, according to Martin Fowler, unit tests have three distinct elements when compared to other types of tests. Thus, unit tests are:
- Low level, focused on small parts of a software system;
- Written by developers, using their own tools;
- Run faster than other types of tests.
However, even the point of test speed is a common point of disagreement among developers. Something they prize the authenticity of tests, at the cost of sacrificing a little speed; while others focusfocus on running tests in thousandths of seconds.
The lines that separate the different types of tests within the pyramid are also not left out of these conflicts. For example, when we add a database operation to our tests, even if it runs in a few milliseconds, can it be considered unitary? When we are using TDD, are the tests considered Black Box or White Box? When continually thinking about testing, are you a Tester or a Developer? To show that not everything fits into dichotomies, Kent Beck has a very enlightening article.
The truth is that there is a lot of discussion about terminologies and where one type of test begins and another ends. My suggestion is to focus on creating meaningful tests, which make you confident when changing your design, not worrying so much about which piece of the pyramid they are in. Don’t care so much if the Test runs in 1 minute or 1 second, as long as it adds value.
Avoid testing implementations, test behaviors
A very common issue for anyone starting out in the testing world is developing a test for each new class or function added to the project. This becomes a problem when the code is going to be refactored and we realize that the tests can no longer be executed correctly because they are too meshed to the production code. Instead, a good practice is to think about testing the behaviors of the application. What our project does is more important than how it does it.
Ian Cooper has an excellent talk about misinterpretations of Test-Driven Development. Although it is more focused on TDD, it is a very enlightening video and addresses concepts that can be used in different testing techniques. Here are some of the points raised by him that greatly improved my view on testing:
Don’t write test for implementation details
The implementation details of a project change too much over time, whether because a class or function has been removed, renamed, or refactored in different files. When we create tests with the premise of “verifying that the function X has been called“, it is quite likely that, if this code is updated, the tests will not even compile anymore.
Not every new class or function should generate a new test
It is not because we have created a new class or function in our project that we should create a test to cover this code. Some code snippets are indirectly covered by behavior-focused testing. This is especially true when the new class/function is internal or private (visible only in a certain scope, without access by another module or client).
Creating new tests is great at increasing code reliability, but creating unnecessary tests makes code hard and difficult to maintain. Always question yourself before adding a new test and don’t be afraid to delete the ones that no longer make sense or that are hindering your development cycle.
A new behavior should generate new tests
When your software receives a new behavior, this, yes, should generate a new test, which covers this new requirement. Try to stay focused on what your application should execute. For example, if the project you are working on is from a calendar software, requirements such as “add an event”, “add a contact”, “invite a contact to an event” should be the requirements to be tested. Knowing whether the project needs to communicate with 1 or 5 classes to add a contact is not the test’s responsibility.
No internal testing
Everything inside your software, whether private, protected, or internal, only concerns implementation details. These types of code have a very high chance of being refactored and, as we mentioned earlier, should not be tested directly. Yes, they will be covered by tests, but in an indirect way.
A good way to do this coverage is to create a public layer (API) that is testable. With this layer, you can test all of the input and output of your software, validating requirements, but without testing the inner layer. Another recommendation is: never leave the encapsulation more permissive to test something. Leaving a public function to facilitate testing is evidence that you are focusing on details, not behavior.
Be careful with the use of Mock
The use of Mocks greatly facilitates the execution of some tests in which we do not want to use a concrete implementation or the simulation of a behavior that can take a few seconds, such as database access. But this tool also comes at a cost: the code being tested needs to know a little more about some details of the implementation of the System Under Test (SUT).
Generally, the configuration of a mock consists of “when function x is called, make it return y”. With this, we return to the point that the test should not know details of the implementation, because, if those details change, such as the function’s name, the tests will no longer compile.
When we have tests that no longer compile, all the security we want to have when refactoring a piece of code goes away. Is the test breaking because I changed the behavior during refactoring? or because, when I made it compile again, I broke the test? And there goes the confiability.
During a video discussing tests, Kent Beck shared his experience on the subject:
“Do you mock absolutely everything? My personal practice is: I mock almost nothing. If I can’t figure out how to test efficiently with the real stuff, I find another way of creating a feedback loop for myself.”
And this sentence brings me to the next point:
Mock is not the only way to simulate behavior
Although Mock is the best-known way to simulate behaviors in Tests, it is not the only way. In another excellent article called “Mocks aren’t Doubles“, Martin Fowler mentions the definitions of each of the 5 Test doubles that exist in software development, created by Gerard Meszaros.
These other test doubles give more flexibility and control, while hiding the implementation details of the test code a little more. I also agree that we can simplify these 5 doubles into just 2 doubles based on the behavior you want. In my personal experience, I try to use the real implementation whenever I can. If it is not possible, I create a fake representation and my final attempt is to mock it.
Good tests lead to good design (and vice versa)
While developing using TDD, I found myself questioning the software design more often than usual. Simple questions like “how would I test this?” or “the tests are getting too complex, is this class doing too much?” help a lot to create better code.
Concepts such as Clean Architecture, SOLID principles and Pair Programming fit very well with all this environment thinking about tests. It is very difficult to test a code base that does not have good design: it is complicated to replace concrete implementations with doubles, the configuration for simple tests is immense and it will probably be necessary to rely more on manual and UX testing than Unit Testing.
Developing and thinking about tests is a responsibility of all project members, not something to be simply left to testers. A well-tested code base brings peace of mind to everyone on the team.
Developing tests will make your project more reliable, easy to refactor and introduce new features. This will also make you grow a lot as a developer, providing more tools and techniques for you to create better code.
The idea of this article was to share some techniques and insights that I have accumulated during my journey to learn more about tests, so that you do not stumble on the same issues that I had difficulty with. In addition, this article is to warm up the discussion of all the importance that this subject has. I believe that adding meaningful tests to your project, even if you are sure which techniques to use, is better than having no test at all.