Testing your code

Everybody writes tests when developing code, it is common for somebody just beginning with writing software to write their tests in an ad-hoc way, and once the specific functionality they have been developing has been completed, they delete their test.

The problem with this is that if we ever want to make further changes to the functionality, we need to recreate the test(s), or risk breaking it.

Scientists are often the worst culprits of this practice, and yet it is scientists who stake their reputation on the results of these codes.

A quick read of Top 12 Reasons to Write Unit Tests by Eric M. Burke and Brian M. Coyner should be enough to convince you that writing tests is not only plays a part in making you more effective as a coder, but that it is fundamental to having confidence in the results that our software produce.

A basic test

Python's unittest module provides all of the framework needed to write simple unit tests of our code's building blocks.

There is a good introduction available on the python-guide website. Some useful suggestions from the site include:

  • A testing unit should focus on one tiny bit of functionality and prove it correct.

  • Each test unit must be fully independent. Each of them must be able to run alone, and also within the test suite, regardless of the order they are called. The implication of this rule is that each test must be loaded with a fresh dataset and may have to do some cleanup afterwards. This is usually handled by setUp() and tearDown() methods.

  • Try hard to make tests that run fast. If one single test needs more than a few millisecond to run, development will be slowed down or the tests will not be run as often as desirable. In some cases, tests can’t be fast because they need a complex data structure to work on, and this data structure must be loaded every time the test runs. Keep these heavier tests in a separate test suite that is run by some scheduled task, and run all other tests as often as needed.

  • Learn your tools and learn how to run a single test or a test case. Then, when developing a function inside a module, run this function’s tests very often, ideally automatically when you save the code.

  • Always run the full test suite before a coding session, and run it again after. This will give you more confidence that you did not break anything in the rest of the code.

  • The first step when you are debugging your code is to write a new test pinpointing the bug. While it is not always possible to do, those bug catching tests are among the most valuable pieces of code in your project.

  • Use long and descriptive names for testing functions. The style guide here is slightly different than that of running code, where short names are often preferred. The reason is testing functions are never called explicitly. square() or even sqr() is ok in running code, but in testing code you would have names such as test_square_of_number_2(), test_square_negative_number(). These function names are displayed when a test fails, and should be as descriptive as possible.

  • When something goes wrong or has to be changed, and if your code has a good set of tests, you or other maintainers will rely largely on the testing suite to fix the problem or modify a given behavior. Therefore the testing code will be read as much as or even more than the running code. A unit test whose purpose is unclear is not very helpful is this case.

A simple function and associated test can be defined in just a few lines using Python's unittest module:


In [ ]:
%%script python

import unittest


def my_function(x):
    return x + 1


class Test_my_function(unittest.TestCase):
    def test_plus_one(self):
        self.assertEqual(my_function(3), 4)

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

Multiple units in a test case

Most of the time though, our functions are more complex than my_function, with different code paths depending on input and state, and sometimes depending on contents on disk.

In these cases, we try to break the TestCase subclass into the objects or functions that we wish to test, and have multiple testing methods within the case to cover all of the permutations:


In [ ]:
%%script python

import unittest


def ordinal(number):
    number = int(number)
    endings = {1: 'st', 2: 'nd', 3: 'rd'}
    if 10 <= number % 100 < 20:
        ending = 'th'
    else:
        ending = endings.get(number % 10, 'th')
    return ending


class Test_ordinal(unittest.TestCase):
    def test_0(self):
        self.assertEqual(ordinal(0), 'th')

    def test_1(self):
        self.assertEqual(ordinal(1), 'st')

    def test_2(self):
        self.assertEqual(ordinal(2), 'nd')

    def test_3(self):
        self.assertEqual(ordinal(3), 'rd')

    def test_4(self):
        self.assertEqual(ordinal(4), 'th')

    def test_11(self):
        self.assertEqual(ordinal(11), 'th')

    def test_20(self):
        self.assertEqual(ordinal(20), 'th')

    def test_21(self):
        self.assertEqual(ordinal(21), 'st')

    def test_111(self):
        self.assertEqual(ordinal(111), 'th')

    def test_121(self):
        self.assertEqual(ordinal(121), 'st')
    
    def test_str(self):
        self.assertEqual(ordinal('2'), 'nd')
    
    def test_non_castable(self):
        with self.assertRaises(ValueError):
            ordinal('not a number')


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

Testing our classroom functions

The functions we defined previously have been put together into a package in https://github.com/pelson/tutorial_classroom, which we have already cloned into the tutorial_classroom folder.

Let's create a new branch called my_classroom so that we can make a few changes.


In [ ]:
%cd tutorial_classroom
!git branch my_classroom upstream/master
!git checkout my_classroom

In [ ]:
!git status

Explore this branch, familiarising yourself with its contents.

Notice that there is a setup.py, and a directory called classroom:


In [ ]:
%ls

Inside classroom there is a __init__.py and a tests folder and that __init__.py implements the functions we developed earlier:


In [ ]:
%ls classroom
%pycat classroom/__init__.py

Finally there is another sub-package in our classroom package called tests, and within there, a mirror classroom sub package:


In [ ]:
%ls classroom/tests
%ls classroom/tests/classroom

Each function in classroom/__init__.py has a file called classroom/tests/classroom/test_<function_name>.py where the tests for that function reside.

The load_classroom function has already been tested using the sample data found in the classroom/tests folder:


In [ ]:
%pycat classroom/tests/classroom/test_load_classroom.py

Install the classroom module as a developer:


In [ ]:
!python setup.py develop --user

We should now be able to import the classroom module, and run the test:


In [ ]:
import classroom

!python -m unittest --verbose classroom.tests.classroom.test_load_classroom

Exercise: Go through the remaining test modules at classroom/tests/classroom/test_*.py and write tests for each of the functions we have defined in classroom/__init__.py.

Note: Because all of the functions take classroom as arguments, there should be no need to use sample data from disk to write these tests.

If you ever need to assert something about a numpy array, be aware that numpy.testing defines most of the common assertions, including equality as well as approximate equality for floating point comparison.

Intro | Previous | Next























In [1]:
%run resources/load_style.py