Python Unit Test
About test driven development
Test driven development is about first writing tests. And then writing code. It is like when you start a course you get a pack of old exams and you only study stuff to pass the old exams. When you then have your real exam you should be able to pass it if you were able to pass the old exams.
See also Python Doctest And Docstring for another approach to testing in Python.
Motivation
Since I cannot phrase it better than Martin Aspeli does in [1] I'll just quote him. He says Unit tests are:
- The only way of even remotely convincing your customers and friends your code doesn't completely suck.
- The only way of making sure ... you don't break things without realising it.
- The only way of making sure ... you don't re-introduce bugs you thought you'd fixed.
- Usually a way of saving time in the long run...
I especially like the first one: "Unit tests are the only way of even remotely convincing your customers and friends your code doesn't completely suck". I agree: you cannot know that you code isn't suckish unless you test it. Also test driven development is an excellent way of minimizing code encumbrance.
Algorithm
There are many ways of explaining test driven development, I think in this way:
- Think of what your program should do and how you can test that.
- Write a test. The test should cover cases that users will do, f.x. "if a user presses Exit then the program should shut down", "if a user tries to read a file that is protected there should be an error explaining that he cannot read the file". And so on. (In the beginning there will only be tests and no programs - all tests will of course fail in the beginning!)
- Run the tests.
- Only write program code that makes a test that fails pass.
- Run the series of tests again. If it fails goto step 4, else goto step 6.
- If you encounter a bug that is not covered by the tests add a test for it (thus goto step 1).
- Refactor your code (meaning clean up code without really changing anything).
References
How to work with test driven development in Python: an implementation of Rövarspråket
This chapter is heavily inspired by the chapters [6] and [7] in the book Dive into Python by Mark Pilgrim
Write the test first
There are a few things you should know when it comes to testing in Python. First of all there is a battery included in Python that facilitates Test Driven Development: the unittest module. And this unittest module uses an ugly case of name magic (like everything else in the python world it appears).
The magic name pattern in unittest is as follows: a class that inherits from unittest.TestCase have all its functions that are called test* (for example testDivideByZero) becomes part of the test.
I want to implement RövarSpråket (see f.x. [8]). So I write a test for it:
import unittest import rovar class CaseCheck(unittest.TestCase): pairs = [['Test', 'TOTesostot'], ['IBM', 'IBOBMOM'], ['fooBAR', 'fofooBOBAROR'], ['XYZZYX', 'XOXYZOZZOZYXOX'], ['emacs', 'emomacocsos'], ['5', '5'], [, ]] def testKeepUpper1(self): """make sure that 'Test' turns into 'TOTesostot', 'IBM' into 'IBOBMOM' and so on""" for [i,o] in self.pairs: self.assertEqual(rovar.enrov(i), o) def testKeepUpper2(self): """make sure that 'TOTesostot' turns into 'Test' and so on.""" for [i,o] in self.pairs: self.assertEqual(rovar.derov(o), i) if __name__ == "__main__": unittest.main()
The test is pretty straightforward and the second magic in it is really the final line: unittest.main(). Apparently it executes all tests in all classes that inherit from the magic mother-class. Also it uses the magic name pattern described above to perform the tests. It's not beautiful but it's ok.
Of course we need a file called rovar.py:
def enrov(item): pass def derov(item): pass
Now running the tests should fail - as expected:
>python rovartest.py FF ====================================================================== FAIL: make sure that 'Test' turns into 'TOTesostot', 'IBM' into 'IBOBMOM' ---------------------------------------------------------------------- Traceback (most recent call last): File "rovartest.py", line 24, in testKeepUpper1 self.assertEqual(rovar.enrov(i), o) AssertionError: None != 'TOTesostot' ====================================================================== FAIL: make sure that 'TOTesostot' turns into 'Test' and so on. ---------------------------------------------------------------------- Traceback (most recent call last): File "rovartest.py", line 31, in testKeepUpper2 self.assertEqual(rovar.derov(o), i) AssertionError: None != 'Test' ---------------------------------------------------------------------- Ran 2 tests in 0.001s FAILED (failures=2)
Making the module
# my rovarpackage
# globals consonants = 'bcdfghjklmnpqrstvwxyz' def enrov(item): for c in consonants.lower(): item = item.replace(c, '%so%s' % (c,c)) for c in consonants.upper(): item = item.replace(c, '%sO%s' % (c,c)) return item def derov(item): for c in consonants.lower(): item = item.replace('%so%s' % (c,c), c) for c in consonants.upper(): item = item.replace('%sO%s' % (c,c), c) return item
Now it should work - right?
>python rovartest.py F. ====================================================================== FAIL: make sure that 'Test' turns into 'TOTesostot', 'IBM' into 'IBOBMOM' ---------------------------------------------------------------------- Traceback (most recent call last): File "rovartest.py", line 24, in testKeepUpper1 self.assertEqual(rovar.enrov(i), o) AssertionError: 'XOXYOYZOZZOZYOYXOX' != 'XOXYZOZZOZYXOX' ---------------------------------------------------------------------- Ran 2 tests in 0.002s FAILED (failures=1)
Ooops. I had an extra y in my consonants variables (how unexpected :D), let's fix it and then we get:
.. ---------------------------------------------------------------------- Ran 2 tests in 0.002s OK
Finding bugs -> more tests
Suppose we would discover that enroving "bob" would lead to "bobobob" and then deroving it would lead to "bobob". If that had been the case we would indeed have a bug. So we could add some tests for it by adding the following lines:
tricky = [['Bob', 'BOBobob'], ['robot', 'rorobobotot'], ['ror', 'rororor'], ['kalasfint', 'kokalolasosfofinontot']] def testTricky1(self): """make sure that 'bob' turns into 'bobobob', etc""" for [i,o] in self.tricky: self.assertEqual(rovar.enrov(i), o) def testTricky2(self): """make sure that 'rororor' turns into 'ror' and so on.""" for [i,o] in self.tricky: self.assertEqual(rovar.derov(o), i)
Now we'd get
>python rovartest.py .... ---------------------------------------------------------------------- Ran 4 tests in 0.002s OK
References
This page belongs in Kategori Programmering.
This page belongs in Kategori Test.