Friday, April 20, 2007

Python and Test Driven Development - Part I

Briefly, Test Driven Development is the strategy of starting the development process by the test cases and then provide the software that satisfies these tests, not the other way around.

Maybe you have been already charmed by the Test Driven Development and its innumerable virtues.

If not, I strongly recommend that you take a look at Wikipedia's (http://en.wikipedia.org/wiki/Test_driven_development) which is a good place to start and has a lot of external links.

The following UML activity diagram depicts the Test Driven Development process.

Start writing the tests, run the tests to verify that the test you have just written fails, write the code that satisfies it and the refactor the code to keep it tidy, clean an minimal.

Personally, the main advantage I've seen so far is that you focus your destination quickly and is much difficult to divert implementing options in your software that will never be used and wasting your precious time so much needed in order to arrive to the end of the project on time. To cut short what could be a very long story, I don't want to debate now if Test Driven Development could be applied to any project. I think that, as well as any other technique, you should use your judgment and expertise to recognize where it can be applied and where not. But having this in mind: there's no silver bullets.


1 Unit testing

Unit is the portion of your software being tested. You should also apply your experience to determine, in each case, what a unit can be. Sometimes it is a class, a set of classes or even a a method. In general, the tools are called xUnit where x is your favorite language (Java, Python, C++, VisualBasic).


2 Test suites

Tests are composed into suites. Suites are a convenient way to keep related tests together and simplifies running all of the tests in every iteration of the process. Normally, all of the tests should be run to verify that nothing else is broken.

3 Test runners

We will be using a python example. Also, assume that we have already written some tests classes stubs containing some tests placeholders. These classes are ATest, BTest and CTests, each in its corresponding module inside the tests subdirectory. These will help to maintain the separation between code and tests. They will test the correctness of classes A, B and C respectively.

#! /usr/bin/python

import sys
import unittest

class ATest(unittest.TestCase) :
def test1(self) :
self.assertEqual(1, 1)

def test2(self) :
self.assertEqual(1, 1)

if __name__ == '__main__':
unittest.main()

The same for the other classes.

In python, when you are writing a test driver you write something like this

#! /usr/bin/env python

import sys
import unittest

from tests.ATest import ATest
from tests.BTest import BTest
from tests.CTest import CTest
from tests.moretests.DTest import DTest

if __name__ == '__main__':
unittest.main()

This is a test driver that imports some classes to be tested, and the unittest discovers all of the test cases using introspection.

Nevertheless, there are some drawbacks with this approach:

  • we need to create a test driver like this in any project
  • we need to maintain the driver and if a new class is created in the project it must be added to the driver
  • the names of the classes must be known in advance

Thus, we will try to address this problems creating a generic test driver, independent from the project we are working on, and this driver will generate automatically the Test suite accordingly to a given pattern of classes and a path to look for the modules.

With all of this in mind, we try our second driver. This driver uses the function find which emulates partially the behavior of the Unix find command (see downloads).

#! /usr/bin/env python

import sys
import os
import re
import unittest

from find import find

def loadTestsFromPath(dir='.', pattern='*Test.py', maxdepth=None) :
modules = []
for f in find(dir, pattern, maxdepth) :
mo = os.path.basename(f)[:-3]
mp = re.sub('^\./?', '', os.path.dirname(f)).replace(os.sep, '.')
if mp:
mp += '.'
modules.append(mp+mo)

return modules


if __name__ == '__main__':
ts = unittest.TestSuite()
for m in loadTestsFromPath('.') :
ts.addTests(unittest.TestLoader().loadTestsFromName(m))

unittest.TestProgram(defaultTest='ts')

Now the situation is much better

  • this is a generic driver, there's no need to maintain it with the target project
  • the names of the classes are not included in the driver
  • automatically the driver finds the existing test cases in the filesystem

We still need some improvements, but it is pretty usable right now. The possible improvements are

  • we require the ability of passing command line parameters to obtain the maximum flexibility, for example the path to start the search and the maximum depth to continue searching
  • this command line options should be integrated with the unittest options
  • the pattern should be variable

These improvements will be the subject of the next part: Pyhton and Test Driven Development - Part II

No comments: