This article is a revised version. It was previously published at refactoring-legacy-code.net published.
Is there a difference between TDD and test-first?
Time and again, I see discussions about how to properly test software. The realization that automated tests are necessary seems to have gained acceptance in the meantime. I no longer hear developers seriously claiming that automated tests are a waste of time, too complicated, impossible in their project or whatever the arguments used to be. Automated tests are obviously recognized by all developers as the most important contribution to the value of correctness.
This is the first step. However, considering automated tests to be useful and actually writing tests regularly are two different things. Of course, automated tests in a legacy code environment are a different challenge than in a greenfield environment. Methods and classes with unclear responsibilities and full of dependencies are not easy to test automatically. However, developers also find it difficult in the green field. They like to discuss the sense or nonsense of certain practices and their details instead of just doing it. And there is clearly no generally recognized process that describes the steps that need to be taken on the way from requirements to code. "TDD" some may now cry. Well, that's better than just coding away. Consistent Decomposition of requirements and Draft are activities that I cannot discover in the vast majority of teams and yet consider essential. For good reason, together with my colleague Ralf Westphal, I worked with Flow Design developed an approach and design language to close this gap.
But let's stick to the topic of automated testing. My observation is that the terms are already mixed up here. As long as it is not clear to everyone involved in a discussion what terms such as "unit test", "TDD" or "test-first" mean, the discussion will not be productive and will instead become entangled in misunderstandings. I would therefore like to use this article to explain what the individual terms mean to me. We can then discuss other meanings with the aim of reaching a consensus on how the terms are meant. We can then begin to discuss the sense or nonsense of certain details.
Values
To ensure that software correctly implements the requirements of the product owner, it must be tested. The value of the Correctness is therefore only achieved when we test software, whether automatically or manually. Perhaps we will get to a point in computer science where correctness can be proven in other ways. At the moment, there is no way around testing. In order to also emphasize the value of Production efficiency the tests must be automated. This results in the practice of automated testing. The values originate from the value system of the Clean Code Developer Initiative.
As developers we write Automated teststo increase the certainty that the requirements have been implemented correctly. Frameworks such as JUnit, NUnit, Mocha, etc. are used to automate the tests. An automated test is therefore a piece of software, usually a method, which runs through part of the implementation and makes assumptions about the result. These assumptions are checked automatically so that the test can indicate success or failure. Automation makes it possible to run the tests repeatedly without incurring significant costs. This is one of the differences to manual testing. Manual tests cost a lot of money and are therefore carried out as rarely as possible. The second difference is that automating the tests ensures that the test cases are guaranteed to check the same assumptions each time the tests are executed. Manual tests often suffer from the fact that, despite written test instructions, not all assumptions are checked exactly. It takes a lot of discipline and very good organization to carry out tests manually.
Automated test
If a test case is automated, the result is a Automated test and not necessarily a Unit Test. Even though test frameworks are sometimes called "unit test frameworks", this term is actually incorrect. It should be called "Automated Test Framework" or similar. The frameworks are used to automate tests.
With such a test, both Unit tests as well as Integration tests and even System tests be automated. All three are therefore orthogonal to the term automated test. Instead, these terms describe which sections of a software system are tested. The following figure shows a tree of functional units with their dependencies.
This tree consists of leaves and nodes. The leaves are formed when the IOSP are referred to as operations, the nodes as integrations.
Unit Test
A Unit Test is responsible for Unitor also Functional unitto be tested in isolation. If one of the sheets is tested, it is a unit test, as the sheets have no further dependencies. A leaf is already an isolated unit. Before we look at how a node can be tested in isolation with a unit test, let's look at integration tests.
Integration test
If one of the nodes is tested, this is an integration test. The node is tested including its dependencies. This means that you are not testing a unit in isolation, but including its dependencies. The aim of integration tests is to find out whether the integration of the components works correctly. As a rule, the dependencies must also be tested during an integration test, as otherwise integration will not take place if all dependencies are replaced by dummies.
In individual cases, it may make sense to replace a very small section of the dependencies with dummies, e.g. to eliminate very expensive and/or complex resource accesses.
System test
System tests are a special form of integration test. A system test is used to test the entire software system including all dependencies. In particular, testing is carried out through the Ui and real resources such as databases or special hardware are used. In integration and system tests, none of the components are replaced by dummies in their pure form. However, as with integration tests, it may also be necessary to replace a few details with dummies.
With interfaces and dummies for unit testing
Sometimes there is a desire to test a functional unit that has dependencies in isolation. The challenge here is therefore to turn a node into a unit. The node must be freed from its dependencies for a unit test. Interfaces and dummies are used for this purpose. This isolates the node and only the component of the node can be tested in a unit test without the dependencies having any influence. However, care must be taken here: such a test is no longer an integration test, as the real dependencies are not integrated in the test, but dummies that may behave differently in the test than the originals they replace.
The following illustration shows a node whose dependencies are all replaced by dummies. In this way, a node can also be tested in isolation in a unit test. However, this is only necessary if the node contains logic that could otherwise only be tested with an integration test. If you follow the IOSPunit tests on nodes are generally not required.
Resource test
Some colleagues no longer refer to a test that accesses an external resource such as the file system as a unit test. I think this definition is wrong! Of course, a functional unit can be a unit and access a resource at the same time. A method that receives a file name and returns the contents of the file is a unit because it has no dependencies on other methods of the solution. Why should automated tests of this method not be called unit tests? Colleagues are mixing up two aspects here. On the one hand the consideration of the dependency structure and on the other hand the access to external resources. Both aspects are orthogonal, i.e. independent of each other. I can realize a resource access in a method, then this method is a unit and can be checked with a unit test. However, resource access can also be realized with many interdependent methods, in which case I can apply an integration test at the top level. To differentiate, I therefore refer to a resource test when a functional unit that accesses an external resource is tested. Both a unit test and an integration test can therefore be a resource test at the same time. To test resources, see here another article.
We now have the following terms available:
- Automated test vs. manual test - statement about the automation of the tests
- Unit test vs. integration test vs. system test - statement on dealing with dependencies
- Resource test - statement about access to external resources
TDD vs. test-first
The two terms are missing TDD (Test Driven Development) and Test-first. I'm not sure whether these terms are used interchangeably by some colleagues. For me, there is a significant difference. Test-first describes that an automated test is created first before functionality is implemented. This contrasts with test-after. Here, the functionality is implemented first and only then are automated tests created.
Test Driven Development describes more than just test-first. With TDD, the tests are also created before implementation. However, the tests are also intended to develop the structure of the software. Put simply, you start with a test, implement just enough to make the test green and then refactor if necessary to eliminate the violation of principles. For example, if I have repeated myself in the implementation and thus the DRY principle violated, I extract the similarities. Or I determine that a method has more than one responsibility and thus violates the SRP is violated. Here, too, I extract the responsibilities through a refactoring measure in order to restore compliance with the principles. After each refactoring, the tests can be run again to ensure that nothing has been broken by the refactoring.
The effectiveness of TDD is debatable and I have a clear opinion on this. Before such a discussion, however, there must be clarity about the terms. Test-first is part of TDD. TDD complements the refactoring step and attempts to use this approach to develop an implementation that adheres to the most important principles.