Modern TDD - Component Level
When a System Level Test fails, we have no idea where's the problem: is the problem with the Frontend or with the Backend (which Microservice)? How can we avoid wasting time with inter-team debugging?
Welcome to the premium Optivem Journal. I’ll help you apply TDD to escape the nightmare of Legacy Code. Join our paid community of 160+ senior engineers & leaders for support on your TDD journey, plus instant access to group chat and live Q&As:
Previously, in Modern Test Pyramid - Component Level, we showed why we need Component Level Tests (Component Tests & Contract Tests):
Component Tests - Each team has assurance that their Component works correctly in isolation (i.e., that the Frontend works correctly in isolation, that the Backend/Microservices work correctly in isolation)
Contract Tests - Each team has assurance that their Component can communicate with other Components & External Systems (e.g., assurance that Frontend can communicate with Backend, that Microservice 1 can communicate with Microservice 2, etc.)
The question is, does it matter if we write Component Tests last (i.e., after code)… or if write Component Tests first (i.e., before code)…
📢 On Wed 9th July 17:00 CET, I’m hosting TDD & Unit Tests - Part 4 (Live Q&A):
P.S. If you’re a Paid Member, you’ll receive a 100% discount (see Event Description for instructions on how to redeem your 100% discount).
Approach 0. Not writing Component Level Tests
A team might be practicing TDD - System Level (ATDD), which aligns the whole team regarding business requirements and provides automated feedback whether they satisfied business requirements, whilst also eliminating Manual QA Regression Testing.
This team might NOT be writing Component Level Tests.
Problems solved with System Level Tests
System Level Tests help teams align on requirements and get automated feedback on whether they satisfied the requirements.
Problems not solved with System Level Tests
Teams cannot test their components in isolation - i.e., the Frontend Team can’t test the Frontend in isolation, the Backend Team(s) can’t test the Backend/Microservices in isolation, but must wait for everyone to finish their job to get feedback. The late discovery of bugs in components leads to more expensive rework.
Teams have to wait for everyone to be done in order to know if they can integrate, i.e., are there any integration issues (e.g., mismatching contracts)? The late discovery of integration issues leads to more expensive rework across teams.
Thus, the fundamental unsolved problem is that, due to the slow feedback loop, teams need to do late, expensive rework. This increases the total development cost, and reduces their capacity to deliver new features, i.e., thus limiting the business value that can be delivered.
Approach 1. Writing Component Level Tests last
At some point, the teams see that System Level Tests aren’t enough, that they need Component Tests.
When teams first start writing Component Levels Tests, they naturally tend to write them last (because they were accustomed to Test Last approaches). Thus, even though they might be writing Acceptance Tests first (practicing ATDD), they might still be writing Component Tests last. Their process might be the following:
Write a failing Acceptance Test
Then each team works in parallel
Frontend team writes some code, then when they finish coding, they write some Component Tests & Contract Tests
Backend team(s) write some code, then when they finish coding, they write some Component Tests & Contract Tests
Verify that all the Acceptance Tests pass
Problems solved with Component Level Tests last
The benefit is that each team gets feedback on whether their Component works correctly in isolation (via Component Tests) and whether their Component can integrate well with other Components & External Systems (via Contract Tests)
Problems not solved with Component Level Tests last
The teams may face integration issues due to misunderstandings. E.g. when the teams had a meeting and agreed on their contract, they had a misunderstanding, or perhaps they both had a good understanding but made spelling errors when implementing the agreement. When they write code and then write the Contract Tests last, it provides late feedback to teams regarding their contract mismatches. This means that multiple teams may need to fix their implementation of the integration code and/or update their Contract Tests, resulting in wasted time.
The Component Tests (and Contract Tests) might not be valid; they might not be testing what we expect to be testing. This is because we’ve never seen them fail. When we see passing Component Tests (and Contract Tests) it doesn’t mean anything; they might be always-green tests, which are useless. In that case, they won’t be good at detecting bugs, which means we’ll end up with a higher percentage of failing System Level Tests, thereby wasting more time debugging.
We could partially try to handle this problem by using Code Coverage, to help us discover lines of code that are never executed by Component Tests. This is useful in helping us add some missing tests.
However, Code Coverage can’t help us know whether we’re verifying behavior - lines of code could be executed, but its output might not be asserted at all (or only partially asserted). This is where we’d need Mutation Coverage; however, the problem is that Mutation Coverage is far too slow with Component Tests (because they include I/O), hence it’s impractical.
Thus, in both cases, it leads to wasted time in debugging and late rework, making development more expensive.
Approach 2. Writing Component Level Tests first
How can we address the following problems:
The teams faced integration issues due to misunderstandings of Contracts
The teams wrote invalid Component Level Tests, which didn’t protect them
Let’s look at the root causes and how this can be solved with the Test First approach:
The reason for the late integration issues is that the teams were not aligned on their inter-team contracts (or perhaps they had misunderstandings regarding behavioral decomposition). To ensure that this problem is solved, we need to convert the failing Acceptance Test into failing Component Tests (and Contract Tests) so that it specifies what behavior each Component needs to satisfy and how each Component will integrate with other Components
The reason why the Component Level Tests may be invalid with the Test Last approach is that we never saw them fail. To ensure that the tests are valid, we need to see them fail (for the right reason) before we start coding, i.e., Test First.
Problems solved with Component Level Tests first
By practicing TDD at the Component Level, we help ensure that:
The teams are aligned in decomposing the behavior of an Acceptance Test into Component Level Tests, thus minimizing/eliminating late integration issues
The teams have assurance that their tests are valid, because they saw the tests fail before implementing the code
TDD - Component Level - Component Tests
Here’s an overview of TDD at the Component Level.
As we can see, for one failing Acceptance Test, we need to see apply TDD within the Component.