Notes about the Test-Driven Development
Notes to my future self about the Kent Beck's book TDD by Example.
Part I - The Money Example
Kent Beck show us how he works in this part. The routine is:
- Quickly add a test.
- When writing the test, we imagine we have a perfect API for our operation.
- If the first test is too much for us to chew at once, test a smaller prerequisite first.
- See it fail.
- Make a little change to make it pass.
- See it pass.
- Refactor to remove duplication.
We quickly see that by following these steps the problem at hand is divided into very small and simple steps. This is useful e.g. if we are distracted a lot. The tests tell us where to continue. Also the small size of the steps reduces "fear" (how Beck calls it). However we are not forced to do the steps these small. We can go at any speed. But when things get weird, we can fallback to really small steps.
I'm not very fond of how Beck calls lots of code smells Duplication. The problem is in dependency between the test code and the implementation code. We have to refactor so we don't have any logic shared between the tests and the implementation code. This is the main driver of the design.
From the process point of view, I think it is really important for Beck to keep to-do lists about his task at hand. I mean plain pen and paper to-do lists. It seems to help him focus on any sub-task he likes. If he has any ideas what to do next, he doesn't do it. He writes it as the next steps in his to-do list. I think he is teaching this technique to his students. The important thing to note is that the lists are a complete list of test-cases for the desired feature. Not partial. You know when you are done.
There are these simple strategies for making a test pass:
- Fake it (till you make it). Return a constant at first and gradually replace it with an algorithm.
- Use obvious implementation when it is simple enough.
- Triangulation, which means to write another similar tests which tests for different values. We have to generalize our solution to make both tests pass. We should only abstract when we have two or more examples.
Beck switches from one to other based on his confidence. As soon as he sees an unexpected red bar, he reverts to Fake it mode. Otherwise, when he is quite sure, he just implements the real implementation. If he is completely lost, he uses Triangulation.
Beck explains he often translates his feeling about the code/design into tests. According to him this is a common theme of TDD. If we feel something is not quite right, we try to write a test to explore it.
Finally when his to-do list for the task is empty, it is time to review the design. Do the words and concepts work together? Beck was quite surprised that his code ended up completely different than his other implementations of Money. He thinks it is because he used a different Metaphor (of expression) this time.
Part II - The xUnit Example
Beck presents a common pattern for writing unit tests:
- Arrange / Given - Create some objects.
- Act / When - Stimulate them.
- Assert / Then - Check the results.
Beck warns us to never have coupling between the tests. Tests should be isolated from each other.
Part III - Patterns for TDD
"No time for testing" death spiral - the more stress we feel, the less testing we do. The less testing we do, the more errors we make. The more errors we make, the more stress we feel. Rinse and repeat.
We should try writing asserts first.
Our tests should use evident data, i.e. concrete numbers for the reader to understand the relationship between them. This rule is an exception to common "don't use magic numbers" rule. If the magic numbers in the test are in the scope of the test method, they are fine.
We should always start with a primitive test which tests an operation which does not do anything. This way we keep the feedback loop fast from the beginning. If we write too realistic test instead, we will have to solve bunch of problems at once.
Explanation tests should be used instead of sequence diagrams. Learning tests should be used when you are first trying to work with some kind of external dependency, like a third-party library. When you are fixing a bug, write a simplest possible regression test first, which will fail initially and then make it green.
What do we do with the test that turned out too big? We introduce a smaller child test, which tests the failing part of the bigger test instead. After finishing the child test, we reintroduce the bigger test.
How do we test an object with a dependency on expensive or complicated resource? We create a fake substitute which returns constants, a mock object. How do we test if the object in test communicates correctly with another object? We have him communicate with a test substitute instead. How do we test an exception which is hard to simulate? We throw it in the test substitute.
How do we test that a sequence of messages is correct? We will keep a Log string a assert it.
How do we leave a programming session when we are programming alone? We will leave a test broken. How do we leave a programming session when we are not alone? We leave with all tests green.
Beck continues with xUnit patterns. Some of them are obsolete today, but the following ones still apply. Use assertions in each test. Use @BeforeEach if you don't like the duplication (which is arguably also good for test readers).
Command - what do we do if we have a computation which is overly complicated? We can create an object solely for this computation and invoke it on it.
Value Object - what to do when the object's identity is unimportant and it is widely shared? We can create an immutable value object which creates a new instance for every setter call.
Null Object - how do we represent a special case using objects? We will create a special case object and give it the same interface.
Template Method - we implement a method using other methods when we have a repeated pattern of method calls.
Pluggable Selector - how to invoke a different method for different instances? Store the method name in a variable and then execute the method dynamically. This pattern should not be overused.
Factory Method - how to create object instance dynamically? Create it in a method instead of a constructor.
Imposter - sometimes it is useful to create a different implementation of the same interface for some purposes. Examples are Null Object or a Composite.
What makes tests good? They have to be fast and stable. He presents a large system with 250K lines of production and 250K lines of test code. The whole build takes 20 minutes to run.
Some things which are not so good for TDD:
- Testing GUIs.
- Testing distributed systems.
- Database schemas.
- Compilers
- Generated code is useless to test.
Beck still think some of these can be TDD tested, but he is not quite sure which ones.
Comments