In 2010 Kent Beck, the inventor of Test-Driven Development, recorded a course in the “Pragmatic Screencasts” series.
After ten years, I had the chance to follow the four-set videos, finally! 🎺
In the 2-hours session, Kent develops a small Java client for Tyrant DB (a simple and very efficient key-value store).
While watching Kent’s screencasts, I took some notes and made a couple of reflections, that I want to share with you.
I tried to put every Kent sentence as a quote, to be more understandable.
Otherwise, I tried to express my understanding of his recommendations.
The test list ☑️
First step in TDD for me is always a list of the tests that I want to have passing.
💡 Keep a TODO list, put things there as new ideas or issues arise.
Looking for the fastest feedback possible ⏩
In the beginning especially, look for the fastest feedback going end to end, as quickly as possible.
Tests should tell stories 😮
Test names should tell little stories.
A test should tell a story!
How big is the step we are willing to take? ⾡
Small steps, small tests.
If I make a long step and I fail, I have a cascade of decisions to review: which one was wrong?
TDD as if you meant it…
He writes the first implementation in the very same test, like the “TDD as if you meant it” approach!
…then he extracts methods to have a shorter and clearer test, then finally move the methods to an actual application class.
Isolated Tests 🤞
Isolated tests: tests that leave the world the same way that they found it.
“Isolated test” means something very different to different people!
A good description of this good property of tests is explained in this article https://medium.com/@kentbeck_7670/test-desiderata-94150638a4b3
There’s also this episode dedicated to this specific topic:
Saying “isolated tests” is like saying that they should be deterministic: I can execute the single test and it should pass, I can execute it in a group and it should still pass (and also the other should pass), and I can run them in random order and they should always pass.
Each test should clean up at the end 💚
Each test is responsible to leave the world in the same exact state as it found it.
Each test should clean after its execution, bringing the world to the state it was before the test was run.
Cleaning after and also before is not the right thing to do: every test should just clean after.
He also writes tests that are already green, e.g. when he writes the test for the
In the name of the test he puts the name of the operation under test, e.g.
Guard clauses 💂
I also tend to like this style of handing guard conditions.
Kent explains this style in his book “Implementation Patterns”…
I’m glad to do a lot of design after the tests pass.
Fake it ‘till you make it 🌵
Sometimes I write a second test after the fake implementation, to “force” me to have a real implementation, other times I just slowly replace the fake implementation with a good one that keeps the tests passing.
He seems to tolerate long methods more than I would…
Also, he seems to delay some kind of refactorings
- does not immediately extract all the constants for the Tyrant operation codes
- keeps copying and pasting from test to test
Copy and paste is suspicious… but I want the green bar!
How many test cases?
Should we write a test to verify that
clearcan clear a map even with two objects inside? We already have a test that verifies that clear works with just one element in the map… is it enough?
I make this kind of decision every single moment when I do TDD…
[…] I’m not testing Tyrant DB, I’m testing my client to Tyrant, so I don’t feel I need this new test.
Test case ordering
Reorder tests to move closer tests that belong to the same behavior.
…a test class should read like a story!
Symmetry is an important driver: Kent avoids to extract a common snippet of code because that would introduce asymmetry in some of the methods where the extraction would not be applicable!
Look for frequent feedback 💛
When tests are red I want to do the least to have them green again, then do the design to make it clean!
Frequent feedback means I catch errors much sooner!
Break down a big problem to have feedback sooner.
Calling the shot ⛳️
Before running tests, I call my shots: is this one gonna pass? 🤔 ….. 😮
Comparing TDD styles 🇺🇸 🇬🇧
In Kent Beck’s TDD style, you make the most of the design in the refactoring step, when your new test is green and so are the other tests.
In other styles of TDD, for example Sandro Mancuso’s, where there’s a greater use of mocking, I noticed that you actually do a lot of design (and take many design decisions) when writing the failing test, while you spend a little effort in designing the code while in the refactoring step.
A good example of this different TDD style is in these video episodes by Sandro Mancuso: https://youtu.be/XHnuMjah6ps.
Making a “big” failing test pass gives me lots of opportunities to make mistakes!
The problem is too big to be solved quickly, to reach quickly the green bar.
So, how can we slice the problem to have faster feedback?
I can adopt a bottom-up style, where I think of smaller operations that will be needed in the “complete” solution, and so I start testing those small operations, and then compose the solution out of these small components from the bottom-up.
He adds a test for a more complex case (two elements in the map) and decides to delete the previous simpler case with just one element in the map.
On testing private methods… 🏗
#getNextKey! But those methods should be private!!
After implementing the “complete” solution from the bottom-up, he deletes the tests of the (now) private methods (
#getNextKey), which were useful to drive the implementation from the ground up, but now are no more useful.
…these tests are no longer necessary…they’re like the scaffolding around a building: you don’t need them where you’ve finished.
TDD error conditions and happy path
Question: with TDD you only test the happy path?
No, it’s a choice: you can choose to only test happy path, but you can also choose to test error conditions (“what if…” conditions)
Can I receive a
null for this parameter?
If I was using this code internally and can be sure that
nullcan never be passed, I would not write a null-check test.
If my code was going to be public though, and I open my code to callers that I cannot control, I cannot be sure a
nullcould be passed, adding additional complexity makes sense!
Incremental design 📈
Gradually let the design get more and more differentiated.
See how he starts with an implementation in the test itself!
Take a big task and break it into smaller tasks, to get intermediate and faster feedback.
Permutation of task order is relevant: which tasks you do first makes a difference!
- may give you feedback quicker
- may allow to change the direction of design sooner
Be aware of when you’re making the design decisions and how you make those decisions.
Kent Beck’s TDD game
He propose to try solving a given problem using a different order of design decisions and see how the resulting design is affected and which order gives you more feedback along the way:
- try to write some code with an order of design decisions,
- then erase and start again
- try with another order of design decisions
- until I find an order that is efficient and gives me lots of feedback
Rhythm is important for a developer.
TDD creates a rhythm, at micro-scale, e.g.
- write a test, make it pass
- write a test, refactor, and make it pass
- write a test, make it pass, and refactor
At macro-scale there’s a rhythm too:
- problem statement
This “shaping of time” happens at various scales:
- every two hours
- at the end of the day (take the last hour/half-hour to finish things)
- at the end of the week
- at the end of the month …
like in storytelling.
Do I want to write a test?
Should I write a test for this case?
Every test that you write is an expense (both short-term and long-term) and needs to pay for itself in the feedback that you get from the test:
- informational feedback
- emotional feedback (will it work?)
If you find those feedbacks valuable, go write the test!
Every test that you don’t write is a risk (both short-term and long-term) you are creating (a feedback you’re not getting)
Are you sure that the test will pass? The risk is then tiny!
What if someone changes the implementation and accidentally breaks something that you decided not to test?
TDD needs balance 🙏
Every time you’re writing tests you have
- costs involved (both short-term and long-term)
- benefits involved (both short-term and long-term)
you need to be balancing between all that.
- Management tasks: choosing which tests to write and in which order
- Design tasks: choosing which design decisions to make and in which order
is what makes TDD so difficult, and needs a maturation process.