Shameless green
The goal right now is not to get the perfect answer but to pass the test. We’ll make our sacrifice at the altar of truth and beauty later.
—Kent Beck, “Test-Driven Development by Example”
Any idiot can build a bridge that stands, they say, but it takes an engineer to build a bridge that barely stands. The trick, in other words, is not merely solving a problem, but solving it efficiently.
For the same reason, good software engineers are brutally pragmatic. They don’t write a line of code more than they need to get the job done, and they don’t worry about making that code beautiful unless and until they know it works. In other words, until it passes the test.
In Programming with
confidence, we started writing a retro text adventure in Go, guided
by tests. The first test we wrote was for a function
ListItems
, which shows the player what items are present in
their current location:
You can see here a battery, a key, and a tourist map.
To validate this test, we deliberately wrote a buggy version of
ListItems
that always just returns the empty string. This
should fail any self-respecting test, and so it does. Now let’s see if
we can make it pass.
Here’s one rough first attempt:
func ListItems(items []string) string {
:= "You can see here"
result += strings.Join(items, ", ")
result += "."
result return result
}
I really didn’t think too hard about this, and I’m sure it shows. That’s all right, because we’re not aiming to produce elegant, readable, or efficient code at this stage. Trying to write code from scratch that’s both correct and elegant is pretty hard. Let’s not stack the odds against ourselves by trying to multi-task here.
In fact, the only thing we care about right now is getting the code correct. Once we have that, we can always tidy it up later. On the other hand, there’s no point trying to beautify code that doesn’t work yet.
Let’s see how it performs against the test:
--- FAIL: TestListItems_GivesCorrectResultForInput (0.00s)
game_test.go:18: want "You can see here a battery, a key, and
a tourist map.", got "You can see herea battery, a key, a
tourist map."
Well, that looks close, but clearly not exactly right. In fact, we can improve the test a little bit here, to give us a more helpful failure message.
Using cmp.Diff
to compare results
Since part of the result is correct, but part isn’t, we’d actually
like the test to report the difference between
want
and got
, not just print both of them
out.
There’s a useful third-party package for this, go-cmp
.
We can use its Diff
function to print just the differences
between the two strings. Here’s what that looks like in the test:
func TestListItems_GivesCorrectResultForInput(t *testing.T) {
.Parallel()
t:= []string{
input "a battery",
"a key",
"a tourist map",
}
:= "You can see here a battery, a key, and a tourist map."
want := game.ListItems(input)
got if want != got {
.Error(cmp.Diff(want, got))
t}
}
Here’s the result:
--- FAIL: TestListItems_GivesCorrectResultForInput (0.00s)
game_test.go:20: strings.Join({
"You can see here",
- " ",
"a battery, a key,",
- " and",
" a tourist map.",
}, "")
When two strings differ, cmp.Diff
shows which parts are
the same, which parts are only in the first string, and which are only
in the second string.
According to this output, the first part of the two strings is the same:
"You can see here",
But now comes some text that’s only in the first string
(want
). It’s preceded by a minus sign, to indicate that
it’s missing from the second string, and the exact text is just a space,
shown in quotes:
- " ",
So that’s one thing that’s wrong with ListItems
, as
detected by the test. It’s not including a space between the word “here”
and the first item.
The next part, though, ListItems
got right, because it’s
the same in both want
and got
:
"a battery, a key,",
Unfortunately, there’s something else present in want
that is missing from got
:
- " and",
We forgot to include the final “and” before the last item. The two strings are otherwise identical at the end:
" a tourist map.",
You can see why it’s helpful to show the difference between
want
and got
: instead of a simple pass/fail
test, we can see how close we’re getting to the correct result. And if
the result were very long, the diff would make it easy to pick out which
parts of it weren’t what we expected.
Let’s make some tweaks to ListItems
now to address the
problems we detected:
func ListItems(items []string) string {
:= "You can see here "
result += strings.Join(items[:len(items)-1], ", ")
result += ", and "
result += items[len(items)-1]
result += "."
result return result
}
A bit ugly, but who cares? As we saw earlier, we’re not trying to write beautiful code at this point, only correct code. This approach has been aptly named “Shameless Green”:
The most immediately apparent quality of Shameless Green code is how very simple it is. There’s nothing tricky here. The code is gratifyingly easy to comprehend. Not only that, despite its lack of complexity this solution does extremely well.
—Sandi Metz & Katrina Owen, “99 Bottles of OOP: A Practical Guide to Object-Oriented Design”
In other words, shameless green code passes the tests in the simplest, quickest, and most easily understandable way possible. That kind of solution may not be the best, as we’ve said, but it may well be good enough, at least for now. If we suddenly had to drop everything and ship right now, we could grit our teeth and ship this.
In my book The Power of Go: Tests, we’ll see how to apply this philosophy to software engineering as a whole: building programs as bridges that barely stand up. In other words, the lightest, smallest, simplest, cleanest, and most elegant solutions we can create, guided by tests. Once you really get the hang of this idea, it seems crazy to write software any other way.
So does ListItems
work now? Tests point to yes:
go test
PASS
ok game 0.160s
The test is passing, which means that ListItems
is
behaving correctly. That is to say, it’s doing what we asked of it,
which is to format a list of three items in a pleasing way.
New behaviour? New test.
Are we asking enough of ListItems
with this
test? Will it be useful in the actual game code? If the player is in a
room with exactly three items, we can have some confidence that
ListItems
will format them the right way. And four or more
items will probably be fine too.
What about just two items, though? From looking at the code, I’m not sure. It might work, or it might do something silly. Thinking about the case of one item, though, I can see right away that the result won’t make sense.
The result of formatting a slice of no items clearly won’t
make sense either. So what should we do? We could add some code to
ListItems
to handle these cases, and that’s what many
programmers would do in this situation.
But hold up. If we go ahead and make that change, then how will we know that we got it right? We can at least have some confidence that we won’t break the formatting for three or more items, since the test would start failing if that happened. But we won’t have any way to know if our new code correctly formats two, one, or zero items.
We started out by saying we have a specific job that we want
ListItems
to do, and we defined it carefully in advance by
writing the test. ListItems
now does that job, since it
passes the test.
If we’re now deciding that, on reflection, we want
ListItems
to do more, then that’s perfectly all
right. We’re allowed to have new ideas while we’re programming: indeed,
it would be a shame if we didn’t.
But let’s adopt the rule “new behaviour, new test”. Every time we think of a new behaviour we want, we have to write a test for it, or at least extend an existing passing test so that it fails for the case we’re interested in.
That way, we’ll be forced to get our ideas absolutely clear before we
start coding, just like with the first version of
ListItems
. And we’ll also know when we’ve written
enough code, because the test will start passing.
This is another point that I’ve found my students sometimes have difficulty with. Often, the more experienced a programmer they are, the more trouble it gives them. They’re so used to just going ahead and writing code to solve the problem that it’s hard for them to insert an extra step in the process: writing a new test.
Even when they’ve written a function test-first to start with, the temptation is then to start extending the behaviour of that function, without pausing to extend the test. In that case, just saying “New behaviour, new test” is usually enough to jog their memory. But it can take a while to thoroughly establish this new habit, so if you have trouble at first, you’re not alone. Stick at it.
Test cases
We could write some new test functions, one for each case that we
want to check, but that seems a bit wasteful. After all, each test is
going to do exactly the same thing: call ListItems
with
some input, and check the result against expectations.
Any time we want to do the same operation repeatedly, just with
different data each time, we can express this idea using a
loop. In Go, we usually use the range
operator to
loop over some slice of data.
What data would make sense here? Well, this is clearly a slice of test cases, so what’s the best data structure to use for each case?
Each case here consists of two pieces of data: the strings to pass to
ListItems
, and the expected result. Or, to put it another
way, input
and want
, just like we have in our
existing test.
One of the nice things about Go is that any time we want to group
some related bits of data into a single value like this, we can just
define some arbitrary struct
type for it. Let’s call it
testCase
:
func TestListItems_GivesCorrectResultForInput(t *testing.T) {
type testCase struct {
[]string
input string
want }
...
How can we refactor our existing test to use the new
testCase
struct type? Well, let’s start by creating a slice
of testCase
values with just one element: the three-item
case we already have.
...
:= []testCase{
cases {
: []string{
input"a battery",
"a key",
"a tourist map",
},
:
want"You can see here a battery, a key, and a tourist map.",
},
}
...
What’s next? We need to loop over this slice of cases using
range
, and for each case, we want to pass its
input
value to ListItems
and compare the
result with its want
value.
...
for _, tc := range cases {
:= game.ListItems(tc.input)
got if tc.want != got {
.Error(cmp.Diff(tc.want, got))
t}
}
This looks very similar to the test we started with, except that most of the test body has moved inside this loop. That makes sense, because we’re doing exactly the same thing in the test, but now we can do it repeatedly for multiple cases.
This is commonly called a table test, because it checks the behaviour of the system given a table of different inputs and expected results. Here’s what it looks like when we put it all together:
func TestListItems_GivesCorrectResultForInput(t *testing.T) {
type testCase struct {
[]string
input string
want }
:= []testCase{
cases {
: []string{
input"a battery",
"a key",
"a tourist map",
},
:
want"You can see here a battery, a key, and a tourist map.",
},
}
for _, tc := range cases {
:= game.ListItems(tc.input)
got if tc.want != got {
.Error(cmp.Diff(tc.want, got))
t}
}
}
First, let’s make sure we didn’t get anything wrong in this refactoring. The test should still pass, since it’s still only testing our original three-item case:
PASS
ok game 0.222s
Great. Now comes the payoff: we can easily add more cases, by
inserting extra elements in the cases
slice.
Adding cases one at a time
What new test cases should we add at this stage? We could add lots of cases at once, but since we feel pretty sure they’ll all fail, there’s no point in that.
Instead, let’s treat each case as describing a new behaviour, and tackle one of them at a time. For example, there’s a certain way the system should behave when given two inputs instead of three, and it’s distinct from the three-item case. We’ll need some special logic for it.
So let’s add a single new case that supplies two items:
{
: []string{
input"a battery",
"a key",
},
: "You can see here a battery and a key.",
want},
The value of want
is up to us, of course: what we want
to happen in this case is a product design decision. This is what I’ve
decided I want, with my game designer hat on, so let’s see what
ListItems
actually does:
--- FAIL: TestListItems_GivesCorrectResultForInput (0.00s)
game_test.go:36: strings.Join({
"You can see here a battery",
+ ",",
" and a key.",
}, "")
Not bad, but not perfect. It’s inserting a comma after “battery” that shouldn’t be there.
In the next post, we’ll add the remaining test cases and behaviours, and we’ll learn how to play a thrilling game for all the family called “Red, Green, Refactor”. Don’t miss it!