What, why
I’ve been reading up on TDD and it has struck me as particularly useful methodology to achieve “clean code that works”. TDD encourages writing unit tests to cover all the code (because by definition, you write a test before a line of code is written). Because all your code is covered you are freed from the fear of breakage due to change and can instantly be more confident and productive. Also, the test cases act as a specification in code - very useful.
Python has standard modules, unittest and doctest to help you write test cases. I simply love doctest. It alleviates much of the pain of writing a test case (setup and all) besides acting as “executable documentation”. The unittest module has a Java legacy and is not to my taste. Also, I wanted to find a solution that would help in automated test enumeration (discovery) in my source directories without having to write any “infrastructure” code. One more thing I was looking for was a way to run both unit tests and doc tests together.
After a bit of searching, I found “Nose“. Nose is a clone of “py.test” which I liked better than the original (subjectively). To get a feel of “Nose”, I set up some python test files.
The following is the directory structure and the contents of the files. I’ve put in both unit tests and doc tests in the files to see how “Nose” handles them. Also, the tests are spread across directories. Note that I had to put an “__init__.py” to allow “Nose” to import tests in a subdirectory.
The setup
The directory structure
prashanth@prashanth-desktop:~/tmp$ tree
.
|-- bingo.py
|-- somedir
| |-- __init__.py
| `-- test_another.py
`-- test_prashanth.py
1 directory, 4 files
bingo.py
def boing(a, b):
'''
>>> boing(10, 20)
30
'''
return a+b
def boing1(a, b):
'''
>>> boing1(10, 20)
40
'''
return a+b
test_prashanth.py
def test_a():
assert 1
def test_b():
print "hello"
assert 0
somedir/test_another.py
def test_bingo():
raise Exception('hgello')
Installing “Nose”
sudo easy_install nose
If you don’t have easy_install, head over here to get information on installation.
Running the tests
Now that “Nose” is installed, let us run the tests,
nosetests --with-doctest
The output is
..E.F
======================================================================
ERROR: somedir.test_another.test_bingo
----------------------------------------------------------------------
Traceback (most recent call last):
File "/usr/lib/python2.5/site-packages/nose-0.10.2-py2.5.egg/nose/case.py", line 182, in runTest
self.test(*self.arg)
File "/home/prashanth/tmp/somedir/test_another.py", line 2, in test_bingo
raise Exception('hgello')
Exception: hgello
======================================================================
FAIL: test_prashanth.test_b
----------------------------------------------------------------------
Traceback (most recent call last):
File "/usr/lib/python2.5/site-packages/nose-0.10.2-py2.5.egg/nose/case.py", line 182, in runTest
self.test(*self.arg)
File "/home/prashanth/tmp/test_prashanth.py", line 7, in test_b
assert 0
AssertionError:
-------------------- >> begin captured stdout << ---------------------
hello
--------------------- >> end captured stdout << ----------------------
----------------------------------------------------------------------
Ran 5 tests in 0.057s
FAILED (errors=1, failures=1)
The first line in the output is the “test progress” indication (..E.F) . When a test succeeds, a ‘.’ is written. When a test fails, an ‘F’ is written. When a test throws an Exception, an ‘E’ is written. Very useful to get a sense of progress as a huge test suite being executed.
“Nose” captures the stdout and stderr when a test case fails to help you debug the issue. To learn more about using “Nose” go here.