A thorough introduction to Test Driven Development
The goal of TDD is clean code that works.
You get there by writing new code only if an automated test is failing and then eliminating duplication.
The phases are:
- Red: doesn’t work and might not compile.
- Green: get to a working state quickly by committing whatever sins necessary
- Only write code that is demanded by the failing tests
- Refactor: Eliminate duplication
TDD assists with dealing with difficult situations and code. * The tougher the programming problem, the less ground each test should cover * It helps manage fear * Be test infected to the point that you write more tests earlier and in smaller steps than you ever thought possible
TDD is an awareness of the gap between decision and feedback and allows you to control that gap.
- It’s a predictable way to develop
- It gives you a chance to learn all of the lessons that the code has to teach you
- It provides feedback between decisions and allows you to design organically
- It improves the user experience
- It lets your teammates count on you–builds trust
- It feels good to write
- Allows for software to be shippable every day
- allows for minute-by-minute team collaboration
Chapter 1 - Multi-Currency Money
What set of tests, when passed, will demonstrate the presence of code we are confident will fulfill the desired goal?
- Use a to-do list
- Don’t start with objects, start with tests
- Imagine the perfect interface, write a test for it, and then work backward
If something is red because of a compile error, fix the compile error(s) first. Tackle the issues one-by-one until they are resolved. Failure is progress. By watching each error be resolved then we’ll see progress. It’s better than just vaguely knowing that you’re failing.
Once compile errors are taken care of, implement the smallest change that you can imagine to get the test to pass.
The goal, until the refactor period, is not to get the perfect answer but to get the test to pass. Generalize (remove duplication) after you’ve gotten a green test.
TDD is not about taking teeny-tiny steps, it’s about being able to take teeny-tiny steps.
Duplication & Dependencies
Duplication is the symptom of dependency. When you have a test that was passed by hard coding a value, that is duplication and it is a dependency issue since the test and the code are coupled to one another. You can’t write another test that makes sense, unless you refactor, without creating more dependencies.
Eliminating duplication eliminates dependency.
2 - Degenerate Objects
The TDD Cycle
- Write a test. Design the interface that you’d like to use
- Make it run. Ugly or clean. If the clean version will take a minute, and you know it, make a note of it and get back to the main problem.
- Make it right. Remove duplication that you introduced
First solve the “that works” problem and then solve the “clean code part”. (This is the opposite of architecture driven development)
Strategies for Getting to Green (2 of 3)
- Fake it
- Use the obvious implementation
As soon as you see an unexpected red back, back up, and fake the implementation and then refactor to the right code.
3 - Equality for All
3rd Strategy for getting to green: triangulation
Triangulation only works when you have two examples or more of code that can be generalized. Use it when you are unsure of how to refactor.
It allows you to think about the problem in a different manner.
What axes of variability are you trying to support in the design? Make some of them vary and the answer may become clearer.
A constraint of a value object is that the values of the instance variables never change once they’ve been set. You don’t have to worry about aliasing problems then. All operations must return a new object.
4 - Privacy
Your tests don’t have to test every edge case. We’re not striving for perfection but reducing defects so that we may move forward with confidence. We don’t have to be 100% perfect in order to be confident.
5 - Francly Speaking
It’s ok to copy and paste, even tests, since there are different phases. Speed trumps design for the brief moment. Good design at good times. Make it run, then make it right.
6 - Equality for all, redux
When youy don’t have enough tests, you are bound to come across refactorings that aren’t supported by the tests.
Write the tests you wish you had [to enable refactorings]
To keep your teeth healthy, retroactively test before refactoring.
7 - Apples and Oranges
Use criterion that makes sense in the domain you’re working in (for object and method design). Don’t conform, if you can, the implementation to the language.
8 - Makin’ objects
If classes aren’t doing enough work to justify their existence, eliminate them. However, don’t do so in one step. Do so in several small refactorings, while maintaining a green test suite.
Decouple the tests from the existence of subclasses when dealing with an inheritance hierarchy. You should have the freedom to change inheritance without changing the model code.
9 - Times We’re Livin’ In
Don’t use a class when a string will do and that allows for a common implementation.
Are the teeny-tiny steps feeling restrictive? Take bigger steps. Are you feeling a little unsure? Take smaller steps. TDD is a steering process. There is no right step size, now and forever.
When stuck on something big, work on something small.
To change or not to change
The dogmatic answer is to not interrupt what you’re doing. However, in practice, it is ok to entertain a brief interruption (ONLY a brief one) but do NOT interrupt an interruption.
10 - Interesting Times
Prefer not to write a test when the suite is already red. However, sometimes it is necessary to do so when you’re changing/adding code that is not covered by another test. It is often easier to work in a smaller/quicker cycle with a unit test for the new/revised piece of functionality and then go back up to the test that is higher up. The conservative approach is to back out the change that caused the red spec in the first place.
Reasoning vs Trying
While you can reason about a change for minutes at a time, it is often more efficient to make the change and then run the test suite to see what we learn. Without tests, you have no choice but to reason.
Writing code without a test
It’s ok to write code without a test when it’s only for debug output.
11 - The Root of All Evil
A constructor is not reason enough to have a subclass and it is a good refactoring to replace the references to the subclasses with references to the superclass.
When deleting subclasses (and in other instances), you’ll often need to delete redundant tests.
12 - Addition, Finally
When the design isn’t obvious, fake the implementation. When the object we have doesn’t behave the way we want it to, we make another object with the same external protocol but a different implementation.
Since the iteractions are very quick using TDD, you are free to try the thing that first pops into your head and revert it if it doesn’t work out after some experimentation.
TDD can’t guarentee that we will have flashes of insight at the right moment. However, confidence-giving tests and carefully factored code g ive us preparation for insight, and preparation for applying that insight when it comes.
13 - Make It
First argument to addition is
augend and the second is
addend. Tests that are concerned with the implementation rather than externally visible behaviour shouldn’t/are not likely to live long.
Sometimes when testing, choose parameters that break the existing test(s).
Anytime you are checking classes explicitly, we should be using polymorphism. When methods are named the same in different classes, it’s not easy to differentiate their purpose when parameters are positional instead of keyword based.
Don’t mark a test as done unless duplication has been eliminated. Sometimes it is easier to work forward, to drive out new functionality, to realize an implementation than it is to work backward. Write tests to force the creation of objects (and then write a test for those objects).
14 - Change
Remember to keep responsibilities separate for objects. You can write simple tests to check your assumptions about language operations.
When you make a refactoring mistake, you can write another test to isolate the problem.
When you don’t have to write tests
You can introduce private helper classes without distinct tests.
If you have a private test, or another, small, piece of code that comes about in the context of refactoring, then you don’t always need to write a test for it. If you get to the payoff of the refactoring and all of the tests run, then the code has been exercised (by the existing tests).
If the logic becomes complex, then write a separate test.
15 - Mixed Currencies
When dealing with loose ends/higher-level tests that are red there are two paths: write a more specific test or trust the compiler and follow its tail. In practice, Kent fixes the rippling changes one at a time.
To avoid the ripple effect, start at the edges and work back to the test case.
Sometimes write the test you want and then back off to make it achievable in one step.
16 - Abstraction, Finally
For TDD to make sense (because of the lines of test code), you’ll need to write half the code as before for the same functionality, write twice as much code in the same time period, reduce the defect rate, and or increase the rate and reliability of deliveries.
While spiking/experimenting it si ok to test the guts of the implementation and not just the external behaviour. While not preferred, it can drive you to make changes that are needed.
Keep in mind that it is good to delete experiments that did not work.
17 - Money Retrospective
TDD’s intention is not perfection nor is its goal for the code to be “finished”. However, areas of the codebase that change frequently should be rock solid. Parts that don’t change all that often don’t need as comprehensive of tests nor as eloquent of a design.
Linter’s are useful since they are automated critics that don’t forget.
Try tests that shouldn’t work and see if they do. It can expose issues in your code.
TDD created tests don’t replace performance, stress, or usability testing.
Test measurement devices are statement coverage and defect insertion rate. A gross measure of coverage is the number of tests testing different aspects of the system and the number of aspects that need testing.
One way to improve coverage, and likewise to improve the design of your code, is to take a test and simplify the logic of the program that the tests cover. Conditionals could be replaced by messages, etc.
Instead of increasing the test coverage to walk all permutations of input, we just leave the same tests covering various permutations of code as it shrinks.
Part 2 - The xUnit Example
18 - First Steps to xUnit
Lots of refactoring has this feel - separating two parts so you can work on them separately. If they go back together when you are finished, fine; if not, you can leave them separate.
Another pattern: take code that works in one instance and generalize it to work in many by replacing constants with variables.
One of the worst possible cases for TDD is bootstraping when there’s no testing framework.
While it is not necessary to work in tiny steps, to master TDD you’ll need to be able to work in such tiny steps. Once you’ve reached a level of mastery, then you can start experimenting with step size.
19 - Set the Table
- Arrange - create some objects
- Act - Stimulate them
- Assert - check the results
Arrange is often the same from test to test whereas the second and third steps are unique.
Two Contraints come into conflict, one is performance and the other is isolation. Isolation can make a test suite unperformant and performance can push a test suite not to be isolated.
Don’t couple your tests. It creates nasty side effects when your tests are coupled.
A common pattern: one test can be simple if and only if another test is in place and running correctly.
20 - Cleaning Up After
Refactoring and then having to undo said refactoring shortly thereafter is fairly common. Some wait until they have three or four refactorings before refactoring because they don’t like undoing work. However, spending time on design isn’t a bad thing and doing the refactorings without worrying about whether or not you’ll have to undo them allows you to think about design freely.
21 - Counting
In general, the order of implementing tests is important. Find a test that will teach you something and you have confidence that you can make it work.
22 - Dealing with Failure
23 - How Suite It Is
Duplication is always a bad thing. One of the main constraints on composite is that the collection must respond to the same messages as the individual items.
24 - xUnit Retrospective
Reasons for implementing xUnit yourself: mastery and exploration (try implementing it with a new programming language).
25 - Test-Driven Development Patterns
Testing is a process of evauluation that leads to acceptance or rejection. Testing software, presumably manually, is not the same as having tests since you cannot rerun manual tests efficiently.
TDD allows you to control your stress and fear. Conventionally when you feel stressed, you write fewer or no tests which leads to more defects and thus more stress. This creates a positive, and self defeating, feedback loop. TDD allows you to break out of that loop by providing a means of control and the ability to create a new habit and thus a new positive feedback loop.