It looks like you are accessing this article on a mobile phone.
For a better experience turn your phone on landscape mode to be able to read the code examples!
Testing leads to failure, and failure leads to understanding.
~ Burt Rutan
A few months ago I was looking at the state of TDD courses. I wanted to know a few things:
How many people do actually teach TDD? How good is the material?
I was disheartened to find out that we are still not teaching Test Driven Development, but rather the
Test Driven Development Cycle (red, green, refactor).
You see, to test drive your code, you need a set of skills that will allow you to use this practice:
Code Design (design patterns and the like)
Evolutionary Design
If you don't understand these techniques you will do a poor job test driving any code!
Writing tests is easy. Testing after the fact is easy.
Test Driven Development (or as some people like to call it now Test First Development) is meant as a code design tool,
where we design our system as we go. The “tests” are just a side-effect of our design efforts–when we write a test and it passes
we just validated our design desissions.
In order to be able to design out of thin air, you will have to have “some” knowledge about code design.
You wanted to know how to TDD in 5 minutes, right?
For this post I am going to use the
Ruby Programming Language as it is a very expressive language that won’t be in the way and we can
concentrate on what we are doing, rather than focusing on the intricancies of the language.
I’m also using the inbuild testing library found in Ruby instead of a
3rd party testing library (which are better if you are working on a realy world product).
Here it goes:
Write a failing test
Write enough code to make the test pass
Refactor
If you are doing this you are not doing TDD.
“Wait, what? I wrote a test first, I ran the test, wrote the code, and then, once it was passing, I refactored!
What’s wrong about that?”
Let’s look into an example and maybe I can convince/show you how to do this right.
It’s going to take more than 5 minutes though, I appologise.
If you only wanted the 5 minute rundow, we are done here and you can go do something else, I won’t take
more of your time. If on the other hand you want to see a better way, continue reading.
Let’s start by looking at this little requirement – it’s kept simple, for the moment, so we can build up
our knowledge slowly. You will have noticed some of the words
are highlighted. These are nouns and verbs, which will help us figure out the main concepts
for this functionality.
From those two sentences we can imagine a DockingStation object that can release a bike. We don’t want to
think about the bike right now as we want to concentrate on the DockingStation first (we have the courage of
delaying the Bike definition till later).
We will start writing a test (on the left hand side), which will drive the code (on the right hand side).
When running this test you will notice something, it failed! Well, not really, it actually threw an error,
meaning our test (or the intent of testing) wasn’t even run!
What do we do now? There is a very powerful concept I learned… decades ago (I wanted to say a few years ago,
but no, I am old now): Change the message
How do we do this?
If you look at the error message, what is the first error message that we see?
That’s right NameError: unitialized constant TestDockingStation::DockingStation.
Remember, just change the message!
Okay, let’s address this error message (writing just enough code for it not to fail the same way)
and run the test again.
The message has changed!
This time we have an ArgumentError. This is because the DockingStation expects one argument (the bike)
when we initialize it!
Let’s add the initialization code to the DockingStation and run the tests again.
Progress, our message changed again!
This time it’s a NoMethodError: undefined method 'release_bike', which makes sense, our DockingStation class
has no method release_bike (as a matter of fact, it doesn’t have any methods, apart from initialize).
Making the tests finally pass with one last message change!
Our test passed! 🥳
This final message change made the tests pass. You might not have noticed, but we moved to fast there.
Instead of just adding the release_bike method, we implemented the whole thing. This is bad! But I
wanted to show you as you will be tempted to make such decissions when working on production code. Taking
bigger steps is a slipery slope.
In an example like this one, where the code is very easy to understand it seems almost silly to do all these little steps, but trust me, it pays off.
So, let’s rewind then and do this properly, shall we?
Rewinding – Baby steps are king!
The message has changed!
And have you noticed something? This is actually the first time our test ran and failed, which
is what we wanted it to do all along!
I cannot stress enough how important it is to take this tiny baby steps when we code!
Oh, yes, we are not returning the bike object. That shouldn't be too dificult to fix...
Our test passed! 🥳
This time they passed for real, after we took our baby steps and we corrected that pesky nil.
One of the things you need to fight against when test driving your code is the temptation to jump over
a step. You will make mistakes. Some mistakes you will find with ease. Some mistakes will eat you for
breakfast.
If you manage to keep disciplined and work through your baby steps your chances of overseeing
something will be vastly reduced.
Many TDD instructors will tell you: “If the tests are green you have to refactor!”This isn’t necessarily true.
We do refactor our code only if and when there is a need to do so.
Right now we could argue that the code
is perfect the way it is (it does exactly what we designed it to do), so there is no need. The issue we
have with this code is that we actually have our “production code” inside of our “design code”, and we
ain’t going to ship “tests” to production.
Let’s fix that…
Note: normally you’d have the production code and the test code in different directories (like lib or src and test);
I left it like this to keep things simple.
I have my code side by side like this (in my editor/IDE) –on the left hand side my test, on the right hand side
my production code and at the bottom a terminal where I can run commands (like running the tests).
Normally I would setup a watcher (a script that watches for file changes) to run the tests when
I save my files (but this is out of scope for this blog post).
Let’s look back at our little requirements document about releasing bikes. The first sentence reads Bikes are
parked in the docking stations. 🤔
This would imply that the DockingStation can have no bike, or rather, that we can add a bike to it if it’s
empty… hang on a minute! In our test, when we release a bike from the DockingStation it’s nor really
released, is it?
What should happen in this case? Once we release a bike, we should not be able to release the same bike again
as it’s already gone!
This test basically releases a bike and then, when attempting to release a second bike (by calling
release_bike again) we expect the result to be nil.
But as you can see below, out test failed; A BIKE WAS RETURNED!
This should be an easy fix...
There we got it, now, when releasing a bike our DockingStation is empty.
The code is simple and we designed it as we went along. As it stands, this code could be released as it
is. But we still got work to do…
Now it would be time to park those bikes in the DockingStation!
Jikes, this blog post turned out to be a mini demo of Test Driven Development, appologies…
This is making me think that maybe you’d like a more structured Test Driven Development introducion. Do you?
I’m asking because this weekend I’ve been toying with the idea of releasing a free introductory TDD email course
to introduce you to all the aspects of TDD you’d normally learn in a 2 day course.
Let me know if you’d like me to bring this introduction to TDD to life!