Novice TDD: Race to Green
'Red, Green, Refactor': that's a helpful mantra for writing your code test-first. But how long should you spend on each stage, and how can you effectively separate each stage from the other? The answer: Make it Work, then Make it Good.
Race to Green
Race to Green. Worry about design later.
When you're starting out with Test-Driven Development, it's hard. Why? Because TDD demands you do two things in an unpleasant order: Design and Implement. These are two pretty different mindsets, and messing them together can get you into tangles.
To start out, I advise that you strongly separate the two. When you're writing your first test, think Design. How do I want this to work? What should my interface look like?
For example: let's say we want to write a simple program that allows Superheroes to fight each other. We can worry about the specifics later. For now, let's start from Designing an Interface to your program:
# Make a couple of superheroes
batman = Superhero.new('Batman', 2)
superman = Superhero.new('Superman', 5)
# Make them fight each other
batman.fight(superman) # => returns superman
superman.fight(batman) # => returns superman
Now I've defined my interface, I can write a test that simulates this. Here, I can write a Unit Test that describes the scenario, because the scenario only involves one class (Superhero
).
Once I have a failing test - failing because I haven't implemented the #fight
method yet, say – I need to move to the next stage: Race to Green. Here's the principle:
When Racing to Green, forget about principles of good design: Single Responsibility, Laws of Demeter, and so on. Just make it work, any way you can.
Here's me Racing to Green here:
class Superhero
attr_reader :power
def initialize(name, power)
# Only need power here to go green
@power = power
end
def fight(other)
return self if power > other.power
other
end
end
Refactor
Now that I've gone green in the fastest, simplest way I can, I can think about design.
The best bit about Racing to Green? Now I can mess with my code however I like, trying out new design approaches and learning how well they work: all without worrying about functionality in my program.
My tests will tell me if I break something: they always have my back.
You should be running your tests every ten to fifteen seconds, minimum
That might seem like a lot, but running specs is like a tic for me. Even if I'm sitting thinking about my code, not writing anything, I run them every few seconds. It's reassuring to know my program works, and it frees me up to think about Design and Design only. Run your tests a lot: eventually you'll feel creeped out if you don't run them. That's a good habit.
Fiddling about a bit, let's worry about design (I've annotated my thinking first, then given the solution later):
class Superhero
# I don't like how this is open to everyone
# who needs to read this? Can I/How can I
# ensure only they are allowed to?
attr_reader :power
def initialize(name, power)
# Not using name. Do I need to change my
# interface? What is the point of name
# right now?
@power = power
end
def fight(other)
# Is there any way I can kill this 'if'
# statement? If statements kinda suck
# because they introduce hard-to-follow
# control flow branches
return self if power > other.power
other
end
end
Here's my solution to these design problems. First, let's change the interface to get rid of name
:
# Make a couple of superheroes
# Their power is the only thing
# relevant to my program now
batman = Superhero.new(2)
superman = Superhero.new(5)
# Make them fight each other
# No changes needed here
batman.fight(superman) # => returns superman
superman.fight(batman) # => returns superman
Now let's update the class to solve those design problems:
class Superhero
def initialize(power)
@power = power
end
def fight(other)
[self, other].max_by(&:power)
end
protected
attr_reader :power
end
Nice! I was freed up to Google for a better solution to return a max-powered object from an array, and better-encapsulated my code. My refactor is complete: now I can write another red test, race to green, and separate my design thinking from my problem-solving.