TDD, not DDT: Doing TDD in the right direction
Here at Fandor we try to do test-driven development (TDD). I like to say that either we test first or, if we can’t figure out how, we test second. So when you interview we ask you to test-drive a little code. Watching a couple of dozen people (we change the problem every round of hiring to keep plagiarism down) solve the same little problem is pretty interesting: not only do you learn a lot about each individual and about the problem, but you also get a good idea of how the methods you’re using work best.
In our current round of hiring I’ve noticed an antipattern in how many of our applicants do TDD, which is that they test from the bottom up instead of from the top down. This is old news if you’ve read up on BDD, which says test outside in rather than inside out, but the message seems not to have gotten to everyone so let’s review.
By “bottom up” or “inside out” I mean testing the details first, perhaps after a hasty but not conclusive (i.e. not resulting in any passing tests) consideration of the problem and what details someone thinks they need. In the programming part of our interviews it might go something like this:
(The task: write a program that allows two humans to play chess on a virtual board. Don’t worry about the UI, just write methods that allow an imaginary UI to ask whether a proposed move is valid and make a move.)
“Let’s see … I’ll need a list of pieces that can move. Well, I’ll need a Game first, so let’s make one …”
describe Game do describe '#initialize' do it "constructs a Game" do Game.new.should be_a Game end end end
(makes the class, requires it, etc.)
“OK, now I can write the method. Let’s write a test to be sure that it’s there …”
describe '#pieces_that_can_move' do it "is defined" do Game.new.should respond_to(:pieces_that_can_move) end end
(defines an empty method)
“Now let’s actually test the logic …”
describe '#pieces_that_can_move' do it "is defined" do Game.new.pieces_that_can_move(:white).should =~ [[0, 1], [1, 1], [2, 1], [3, 1], [4, 1], [5, 1], [6, 1], [7, 1], [1, 0], [6, 0]] end end
(implement implement implement)
“OK, now let’s use that to check the player’s move.”
(maybe we have to sit through the respond_to business again, but eventually we get around to writing)
describe '#move_is_valid?' do it "returns true if the given piece can move to the given location" do Game.new.move_is_valid?(0, 1, 0, 2).should be_true # rook's pawn moves forward one square end end
“Oh, wait … pieces_that_can_move doesn’t tell me if the move is valid, too. I guess I need another method.”
Wow, we just wrote a lot of code that doesn’t do what we want! That example for Game#initialize will probably pass no matter what we write, but admit it, it’s worthless — if we don’t have any examples that do anything interesting with a Game, it doesn’t matter if we can construct it or not. TDD backwards is DDT — an insidious poison that will weaken your eggshells and — anyway, it’s bad for you.
It’s probably obvious what I’m going to argue is the right way to proceed: test-drive your code top down. Write an example that tests something that we actually want the program to do for us. For this little exercise we were asked to write methods that do something, so let’s just start with a test of one of those methods. In fact, the last test we wrote, of move_is_valid?, is the first test we should have written.
All right, we’ll delete all of the code other than that test and start over.
“Gee, there’s a lot of code to write to decide where exactly a pawn can move, isn’t there? That’s why I wanted to write some low-level methods that I could put together.”
But, as we just discovered, our big brains don’t always think of the low-level methods that we’ll actually need. Instead, just take it a test at a time and refactor. All it takes to make that first test pass is
class Game def move_is_valid? from_x, from_y, to_x, to_y true end end
That won’t get you very far at the chess club, but let’s just write another test:
describe '#move_is_valid?' do it "returns false if a pawn is to move sideways" do Game.new.move_is_valid?(0, 1, 1, 1).should be_false end end
OK, now we’re forcing ourselves to implement that method a little more meaningfully. Let’s try
class Game def move_is_valid? from_x, from_y, to_x, to_y from_x == to_x end end
That wasn’t so bad. Let’s do it again.
describe '#move_is_valid?' do it "returns false if a pawn is to move three spaces forward" do Game.new.move_is_valid?(0, 1, 0, 3).should be_false end end class Game def move_is_valid? from_x, from_y, to_x, to_y from_x == to_x && to_y - from_y == 1 end end
And so on. After another few tests we’ll be forced to actually represent the pieces on the board, distinguish black from white, distinguish pawns from rooks from etc., and implement all the other rules of chess. We’ll find that some of the code we’ve written doesn’t handle the new tests we think of, but we can refactor to make room for new requirements and our existing tests will catch breakage. We’ll never need to guess what code we need, only write what we need when we need it, and we’ll only write examples that actually capture requirements.
There are of course a lot more nuances to TDD than anyone wants to see crammed in to one blog post. Kent Beck’s TDD by Example is the first and best presentation of a long example with all the trimmings, and The RSpec Book is where to turn when you want “outside in” to mean starting outside the entire application, using Cucumber to express actual user scenarios and then filling in the details with rspec.
But the basic principle is this simple: Take it from the top, not from the bottom. Implement requirements, not guesses.
Occasionally, some of your visitors may see an advertisement here.
[…] TDD, not DDT: Doing TDD in the right direction […]
How I spent my working vacation | Dave Schweisguth in a Bottle
March 2, 2014 at 08:31 Edit