Apr 152010
 

I learned Python for a computer science course I’m taking as an elective and I was introduced to Test-Driven Development. Test Driven Development (TDD) is a programming technique where you first write a test for a piece of code, such as a function called hello_world that returns a string containing “Hello World!”. But! You write the test first, before you write the actual function.

I am now a convert to TDD. For Python, you can either use the built-in Unittest (good but not great) or use nose. I used to be a real fire-from-the-hip type of programmer and I’d just go off and start writing code soon as I came across a problem. For small projects, not a problem, but for bigger ones, things would get really hairy. TDD forces me to focus on planning what every part of my program does and thus prevents messiness. I know from first hand experience that it saves me time. I know, as a programmer, I have a tendency to think any time spent not actually coding is not productive, but a lot more of programming happens in the planning stage.

Without further ado, here’s what TDD looks like for a simple Python program. If you’re familiar with TDD, skim this, cause there’s some interesting and useful shortcuts that make testing and painless as possible and make it much easier to stick to a TDD style.

In Python, this would look like:

import nose
def test_hello_world():
   '''Tests hello_world()
   '''
   assert hello_world() == "Hello World!"

if __name__ == "__main__":
   nose.runmodule()

Let’s take each line in turn. The first line imports the nose library. The second line defines a test function. The next line is a comment. I believe in writing code that’s easy to read, not easy to write–code is read a lot more often than its written, often by you! The next line is more interesting. The “assert” wishes the assert that hello_world() is indeed equal to “Hello World!”. This is the actual “test” bit. The if __name__ line means that if this code is being run by itself and not as an include in another program, then run the code in the if __name__ block. Google if you’d like to know more. nose.runmodule() tells nose to run on the current file only. There’s other ways to call nose, too, such as nose.main() and nose.run() which finds all tests in the directory the file is in that nose.main() is called from. The way tests are found are “if it looks like a test, it’s a test”. So, if you include “test” anywhere in your function name, it’s a test and nose will run it.

Let’s get back to the programming. If hello_world() does not equal “Hello World!”, which it does not in this case, you’ll get thisĀ  if you run it:

E
======================================================================
ERROR: __main__.test_hello_world
----------------------------------------------------------------------
Traceback (most recent call last):
File "c:\python25\lib\site-packages\nose-0.11.1-py2.5.egg\nose\case.py", line 183, in runTest
self.test(*self.arg)
File "<string>", line 4, in test_hello_world
NameError: global name 'hello_world' is not defined

———————————————————————-
Ran 1 test in 0.000s

FAILED (errors=1)
SystemExit: True

Broken, eh? That’s cause we haven’t defined the function yet! But notice something, there’s not a whole lotta information on which test actually ran, and when you get into many tests, it can get confusing, so: let’s change an option with which we run nose and write in the hello_world() function.

import nose

def hello_world():
   '''Returns a string "Hello World!"
   '''
   return "Hell World!"

def test_hello_world():
   '''Tests hello_world()
   '''
   assert hello_world() == "Hello World!"

if __name__ == "__main__":
   nose.runmodule(argv=['nose', '--verbosity=3'])

Ok, so let’s run this thing:

Tests hello_world() ... FAIL

======================================================================
FAIL: Tests hello_world()
———————————————————————-
Traceback (most recent call last):
File “c:\python25\lib\site-packages\nose-0.11.1-py2.5.egg\nose\case.py”, line 183, in runTest
self.test(*self.arg)
File “<string>”, line 11, in test_hello_world
AssertionError

———————————————————————-
Ran 1 test in 0.000s

FAILED (failures=1)

Aha! Improvement! Notice two things: 1. We now get the docstring from the test function showing up to indicate which test was run and what happened, and we’ve stopped having an Error (which is fatal) and we are now getting a Fail, which just means that the assertion failed but that the program would still run. A different kind of bug is often a sign of improvement. So, how do we fix this bug?

It’d help to know the value that failed. There’s two ways to do that. Know the assert line? I can rewrite it this way:

assert hello_world() == "Hello World!", "Function output does not match 'Hello World!' instead it is: " + hello_world()

By adding a comma after the assert equation, I can add a string to be printed if the program should fail. So here’s the new test output:

Tests hello_world() ... FAIL

======================================================================
FAIL: Tests hello_world()
———————————————————————-
Traceback (most recent call last):
File “c:\python25\lib\site-packages\nose-0.11.1-py2.5.egg\nose\case.py”, line 183, in runTest
self.test(*self.arg)
File “<string>”, line 11, in test_hello_world
AssertionError: Function output does not match ‘Hello World!’ instead it is: Hell World!

———————————————————————-
Ran 1 test in 0.000s

FAILED (failures=1)
SystemExit: True

Notice the bolded line. It now tells me what the bad value was so I can fix it, and the bad value is that I missed an ‘o’ in “Hello”. Hooray! This sort of line is often shortened to “assert a == b, a + ‘!=’ + b”. In fact, this is so common, there’s even a function that does this for you, called eq_(a, b). For example, if we had used eq_ instead of the assert statement above, the bolded line in the nose test output would read:

AssertionError: 'Hell World!' != 'Hello World!'

Very helpful! So, let’s change our code to take advantage of this and fix the error:

import nose
from nose.tools import eq_

def hello_world():
   '''Returns a string "Hello World!"
   '''
   return "Hello World!"

def test_hello_world():
   '''Tests hello_world()
   '''
   eq_(hello_world(), "Hello World!")

if __name__ == "__main__":
   nose.runmodule(argv=['nose', '--verbosity=3'])

And run nose test again:

Tests hello_world() ... ok

———————————————————————-
Ran 1 test in 0.000s

OK
SystemExit: False

So, now we KNOW that hello_world is correct and that I’m DONE programming. Here’s another advantage of TDD: changes to the code can be tested to make sure “improvements”, “new features” or “bug fixes” haven’t broken anything else. While you’re programming, you’ll be creating a set of tests that can be simply be run to make sure nothing has been broken by your changes.

Another tip and then you can go off your merry way, developing in a test-driven fashion: often tests are in other files than the ones you’re programming in. To prevent time-consuming switching back-and-forth between making changes and running the tests, you can actually add the file name that you want to be tested to the nose.runmodule call. So, let’s say we seperated the function and the test function into two files:

hello_world.py contains:

def hello_world():
   '''Returns a string "Hello World!"
   '''
   return "Hello World!"

if __name__ == "__main__":
   import nose
   nose.runmodule(argv=['nose', 'test_hello_world.py', '--verbosity=3'])

test_hello_world.py contains:

import nose
from nose.tools import eq_
from hello_world import hello_world

def test_hello_world():
   '''Tests hello_world()
   '''
   eq_(hello_world(), "Hello World!")

if __name__ == "__main__":
   nose.runmodule(argv=['nose', '--verbosity=3'])

So, now if I hit run/compile/debug in either file, the tests will run.

Happy testing!