Documentation for trunk
Other versions: 4.3 4.0 3.27 Older...
KataMinesweeper
An example solution using Python and TextTest
Test Driven Development (TDD) is a
way of programming where you start be specifying a (failing) test,
write code until it passes, then refactor. Repeat. This technique has
been hugely valuable and adopted widely since it was introduced to
the world by Kent
Beck and others. The classic way to perform TDD is to specify the
tests using the same programming language as the code under test,
with the help of a little framework called xUnit. (Replace “x”
with the language of choice). But how would TDD look if you used
another tool instead of xUnit? Is there a way of capturing the
advantages of the approach, but using tests that are easier to write
and maintain?
I begain using xUnit in 2000 and
have been improving my skill at TDD ever since. Recently though I
have been working less and less with xUnit tests, and more and more
with using TextTest tests to drive development. This approach seems
to preserve the original spirit of TDD, ie the tests are still
driving development, but there are some differences and advantages.
When you use TextTest to drive
development, your tests all end up being written in a representation
that is separate from your code, ie a kind of Domain Specific
Language (DSL). Your tests can be read and understood by anybody who
understands the domain of the system under test, that is, they don't
have to be able to read or write program code. You typically don't
write new code when you write a new test, you just specify new
desired program behaviour using this DSL. This makes it quick to add
new tests, and easy to refactor your code later, since the test
doesn't place any demands on the internal structure of your program
(its classes and methods).
With xUnit tests, each test is only
working with a small section of code, whereas a TextTest typically
drives a path through a whole system or subsystem. This means that
you need to use another technique to get fine grained visibility of
what your code is doing. Instead of turning to a debugger, the
approach with TextTest is to insert log statements into your code.
Having your application write a log is not a new idea and there are a
number of excellent logging frameworks out there. TextTest allows you
to not only specify the behaviour of your application in terms your
customer will understand, but also control the application logging
behaviour. When you make a code change, and your program no longer
behaves the way the customer wants, (you introduce a bug), you can
turn to the recorded application log to pinpoint exactly where
program behaviour has deviated from previously. So a test case
consists basically of program input, expected user-readable output,
and an application log.
Another aspect of TDD with xUnit,
is that it in some sense forces you to make good low level design
decisions, and you don't generally get the same direct effect with
TextTest. With xUnit, your code must exhibit loose coupling and high
cohesion before you can extract units of code to test. This is
desireable, and TextTest does not force you to do it, since it tests
at a higher level of granularity. (On the other hand, if you are
starting with poorly structured legacy code, where easily extractable
units are hard to come by, you can get going with TextTest more or
less immediately just by adding some (hopefully) harmless log
statements. But I am not talking about legacy code in this article).
The other thing that TDD with xUnit
does to help with low level design, is that when you define your
test, you are effectively designing the interface to your class. I'm
talking about choice of method names, and what arguments you give
them. This is useful because it forces you to think about the
external interface to your class before you consider the internal
implementation. TextTest doesn't help much with this either. It will
force you to start defining the interface to your whole program or
subsystem, but it won't help on the class level. So when developing
with TextTest we suggest something called “usage first”
design. Start with the external interface to your program, and write
code as if the classes and methods you want are already there. Then
fill in the implementations, always working top down, and defining
the usage of a class or method before your implement it. This takes
some discipline, but leads to a similar effect to using xUnit.
A code
Kata is an idea introduced by Dave (Pragmatic) Thomas. He points
out that specialists in other disciplines do a lot more practicing
than programmers do, in order to improve their skills. Others have
further developed this concept into the idea of a “Coding
dojo”, or a place where programmers meet to practice code Kata
together. The website www.codingdojo.org
is a resource for these communities, and amongst other things has a
catalogue
of various code Kata exercises various individuals and groups have
worked on.
So that is all the theory, how does
it work in practice? In this screencast, I demonstrate a Prepared
Kata of KataMinesweeper,
using TextTest to drive development, in python. During development, I
aim to work in a Test Driven manner, and I want to develop top down,
usage first, in small steps. The idea is that although in real life
you may not be so strictly test driven, or exclusively top down, or
use such small steps, that it is important to know how to do these
things, and they are worth practicing on a toy problem like this.
My aim is to demonstrate what it is
like to do TDD with TextTest, and how you might solve this code Kata
using the python programming language. If you would like to comment
and give me constructive feedback on this screencast, I would love to
hear from you. Please comment on www.codingdojo.org
or the TextTest
mailing list, or the python
mailing list.
The screencast is in four parts,
and I estimate each part will take you about 10 – 15 minutes to
view.
(Note:
This screencast was created using Wink.
In it I use Windows Vista Home Edition, Easy
Eclipse for LAMP version 1.2.2 and TextTest
version 3.9.1.)
Here is
the final code that I end up with: (Click to enlarge)
And
here it is again in plain text in case you can't read that
screenshot:
import sys
def log(message):
print >> sys.stderr, message
MINE = "*"
END_OF_INPUT = "0 0"
class Minefield:
def __init__(self, m, n, minefield):
self.m = m
self.n = n
self.minefield = minefield
def clues_to_str(self, clues):
clues_str = ""
for i in range(self.m):
for j in range(self.n):
clues_str += str(clues[(i, j)])
clues_str += "\n"
return clues_str
def clues(self):
clues = dict.fromkeys([ (x, y) for x in range(self.m)\
for y in range(self.n)], 0)
log("initialized clues dictionary %s" % clues)
for x, row in enumerate(self.minefield.splitlines()):
for y, cell in enumerate(row):
if cell == MINE:
clues[ (x, y) ] = MINE
self.increment_adjacent(clues, x, y)
log("calculated clues %s" % clues)
clues_str = self.clues_to_str(clues)
return clues_str
def increment_adjacent(self, clues, x, y):
for i in (x-1, x, x+1):
for j in (y-1, y, y+1):
if clues.get( (i, j) ) not in [None, MINE]:
clues[ (i, j) ] += 1
def MinefieldReader(inputfile):
next = iter(inputfile).next
while 1:
shape = next().strip()
if shape == END_OF_INPUT:
raise StopIteration()
m, n = map(int, shape.split())
log("found minefield shape %s %s" % (m, n))
minefield = ""
for i in range(m):
minefield += next()
yield m, n, minefield
def main(inputfile, outputfile):
counter = 0
for m, n, minefield in MinefieldReader(inputfile):
counter += 1
print >> outputfile, "Field #%d:" % counter
print >> outputfile, Minefield(m, n, minefield).clues()
if __name__ == "__main__":
inputfile = sys.stdin
outputfile = sys.stdout
main(inputfile, outputfile)
|