Continuous testing makes developing much simpler. If you have a set of unit tests, a continuous testing framework runs these tests in the background whenever you change a source file.

For Python, a variety of tools exists such as sniffer or tdaemon. Note: The following code has been tested for Python 3.3 and Python 2.7. There will be some extra effort if you want to use Python 3.3 with tdaemon of the version I used (as of Aug 22, 2013).

Quick Summary

  1. Run tdaemon to continuously watch for changes:
    tdaemon --custom-args="--with-notify --no-start-message --all-modules"
  2. Implement functionality in the class under test
  3. Implement unit tests with unittest.TestCase
  4. Re-iterate through steps 2.  and 3., going from green to green (refactoring), green to red (new test) or red to green (new functionality)

Class under test

For our setting, I created a small class that represents a triangle. We want to test the method area that returns the area of the triangle using Heron’s formula. The following is the content of the file Triangle.py.

class Triangle:
    def __init__(self, a, b, c):
        self.a = a
        self.b = b
        self.c = c

    def area(self):
        s = 0.5 * (self.a + self.b + self.c)
        area = math.sqrt(s * (s - self.a) * (s - self.b) * (s - self.c))
        return area

Unit tests

The unit tests are created with the unittest framework that normally comes shipped with Python. We subclass unittest.TestCase and declare each test as one method that contains the string test or Test in its method name.

from Triangle import *
import unittest

class TriangleTest(unittest.TestCase):
    def test_area1(self): # right triangle with legs 3,4 and hypotenuse 5
        a, b, c = [3, 4, 5]
        expected = 6
        self.assertEqual(Triangle(a,b,c).area(), expected)
    def test_area2(self): # equilateral triangle with side length 3
      a, b, c = [3, 3, 3]
      expected = 3.8971143
      self.assertAlmostEqual(Triangle(a,b,c).area(), expected, places=5) 
if __name__ == '__main__':
     suite = unittest.TestLoader().loadTestsFromTestCase(TriangleTest)
     unittest.TextTestRunner(verbosity=2).run(suite)

The last lines configure the test suite and set the verbosity level to 2. From the command line, we can run the unit tests manually, already: python TriangleTest.py and get feedback. Documentation on the various assert methods can be found here

Continuous Testing

The motivation of this post is that we do not want to run our tests manually, each time we modify the code. Instead we let tdaemon monitor our files and decide when to run the tests.

For this we need two python packages: nose and tdaemon. Both of them can be installed easily through pip. In order to get pip on Ubuntu, you need to execute one of the following:

sudo apt-get install python-pip # this resolves to Python 2.7 on my system
sudo apt-get install python3-pip # this resolves to Python 3.3 on my system

Afterwards, install nose and tdaemon:

sudo pip install nose tdaemon # for 2.7
sudo pip3 install nose tdaemon # for 3.3

Depending on your flavor of Ubuntu, you may also get “system tray” notifications from tdaemon if you install pynotify:

sudo pip install nose-notify
sudo pip3 install nose-notify

Fixes for Python 3

In my version of tdaemon, there were several issues related to changes from Python 2 to Python 3. All of them occurred in /usr/local/lib/python3.3/dist-packages/tdaemon.py.

  • All print statements need parantheses.
  • In line 263, we should leave out msg and write: except Exception as e, adapting the print statement below to print(e).
  • The import of commands is deprecated, we should use subprocess instead. We replace commands.getoutput with subprocess.getoutput
  • Around line 175, there is a call to hashlib.sha224. We have to explicitly encode the string content as UTF-8: hashcode = hashlib.sha224(content.encode(‘utf-8’)).hexdigest()
  • The method raw_input has been renamed to input in Python 3. We need to change the single occurrence of raw_input around line 46 to input in the file.

I came across an encoding problem that occurred rather infrequently:

UnicodeDecodeError: ‘utf-8’ codec can’t decode byte 0x… in position ….: invalid start byte

Caused by the opening of  a file around line 175: content = open(full_path).read(). As I could not figure out, what went wrong exactly, I decided to add the option errors=”ignore” in order to ignore this problem.

Running tdaemon

Once everything is set up, you need to go into the folder containing your implementation and test module and type:

tdaemon --custom-args="--with-notify --no-start-message --all-modules"

This tells tdaemon to run nose (default) with system notifications (system tray) enabled and with auto-discovery of all modules it can find. If you find that system notifications do not work for you, leave out the option –with-notify.

Afterwards, tdaemon presents the command it will run to you and you need to confirm with y

WARNING!!!
You are about to run the following command
   $ nosetests . –with-notify –no-start-message –all-modules
Are you sure you still want to proceed [y/N]?

The first change you make to any of the affected files will trigger nose:

2013-08-22 09:36:58.409211
..
———————————————————————-
Ran 2 tests in 0.008s
OK

You can quit tdaemon‘s execution with Ctrl-C.

References

  • [1] blog entry that describes the setup for several platforms (not just Linux)
  • [2] tdaemon project site
  • [3] unittest module reference