Per Erik Strandberg /cv /kurser /blog

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:

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:

  1. Think of what your program should do and how you can test that.
  2. 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!)
  3. Run the tests.
  4. Only write program code that makes a test that fails pass.
  5. Run the series of tests again. If it fails goto step 4, else goto step 6.
  6. If you encounter a bug that is not covered by the tests add a test for it (thus goto step 1).
  7. 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.