TDD revisited
src: TDD Revisited - Ian Cooper - NDC Porto 2023 - Invidious - 2024-01-03
Abstract
Fallacies with TDD
Developers write unit tests
Developers write developer tests, not unit tests as per definition.
- lot of people use mocks to replace all their dependencies ⇒ begin to hit the fragile test issue
overspecified software: our tests should focus on the contract, but here they focus on the implementation, which makes them hard to read as there is a lot of setup code
behavior sensitivity: if we change how our code works, a lot of tests may break - we say that our tests are sensitive to changes in the details
If the program’s behavior is stable from an observer’s perspective, no tests should change.
- “behavior is stable”: TDD is a contract-first approach to testing. Behavior in this context means that contract.
- “no tests should change”: in other words, when we change the implementation without changing the contract of what is under test, then the tests don’t change
Tests are coupled to the contract expressed by the code, not coupled to the implementation details, i.e. via mocks that check details
Failure of a unit test shall implicate one and only one unit (a method, module or package).
Failure of a programmer / developer test, under TDD implicates only the most recent edit.
TDD produces developer tests.
The trigger for a new test is a new function
Belief: The function is the System-Under-Test (SUT).
SUT: testing is about confirming the behavior of our functions. We may want to use techniques like parameterized testing to allow us to easily vary input, test edge conditions etc.
function: the desire to test methods on classes in languages that provide access control leads to the question of how to test private methods.
The trigger for a new test is a new behavior.
TDD works against behaviors.
There’s no significantly difference between TDD and BDD now.
Customers write Acceptance tests
They create a significant maintenance burden, means that acceptance testing isn’t worth the cost.
- acceptance tests written using FIT or Cucumber are more expensive to write because you need to translate to inputs, and more expensive to own as they are expensive to change
ATDD is perilous because it implies that TDD does not deal with the acceptance criteria for user stories.
It doesn’t matter if you are test first or test last
Implicitly modeling occurs before development. This may be a lightweight process like CRC cards, or heavier exploration via UML.
The feedback loop is long. This design takes to implement. In a RAD environment, this may be a few days, it might be following iteration though or beyond.
If you write code that is not needed by the given requirements, you are engaging in speculation. Most likely you will be wrong. The code will not be needed or you will have to re-work that code.
You need to have:
- design help
- scope control
Only write production code in response to a test. Only write a test in response to a requirement (user story & acceptance criteria).
You need a need to think about design, you need a method for scope control
Test first is design-by-contract. We are guided by the behavior required of the system.
If we test first we don’t end up with speculative code. We know when we are done, and our code is as simple as it needs to be, but no simpler.
Test first is about design help and scope control, whereas test last is about regression proofing yourself.
You want 100% test coverage of your code
Not all of code should be driven by TDD. TDD is useful where it can provide fast binary feedback. If it’s not the fastest way to provide feedback, use something else.
Fallacies ←> Principles
- Developers write Unit tests ←> Developers write Developer Tests
- The trigger for a new test is a new function ←> The trigger for a new test is a new behavior
- Customer write acceptance tests ←> Customers write Acceptance Criteria
- It doesn’t matter if you write test first or test last ←> Only write production code in response to a test
- You want 100% test coverage of your code ←> Not all code should be driven by TDD