SWC2013TDD » History » Version 18

« Previous - Version 18/19 (diff) - Next » - Current version
Chris Cannam, 2013-02-08 12:31 PM


Test-driven development outline

Note: This section went very badly in the MAT workshop. The following has been reworked subsequently, as a possible outline for future workshops.

We assume that the "intro to Python" section has at least introduced how you would run a Python program and compare the output against an external source of "correct" results; also that the audiofile section has discussed what an audio file consists of.

Motivation

We'll refer first back to the "intro to Python" example, with the text file of dates and observations.

Date,Species,Count
2012.04.28,marlin,2
2012.04.28,turtle,1
2012.04.28,shark,3
# I think it was a Marlin... luis
2012.04.27,marlin,4

We have our program that prints out the number of marlin.

$ python count-marlin.py
2
$

We can check this against some human-generated output, or the result of "grep" or something if the program is simple enough, in order to see whether it produces the right result. But what if we change the program to add a new feature -- will we remember to check all the old behaviour as well and make sure we haven't broken it? What if the program as a whole is so complex and subtle that we don't actually know what its output will be?

We need to do two things:

  1. automate the tests, and
  2. make sure we test the individual components that the program is made up of (so we can be confident of its behaviour even when we don't know what the program as a whole should produce)

Simple unit tests with Nose

Imagine we're dealing with audio data, and we have a program that loads data from an audio file and then (like many spectral methods) chops it up into fixed-length frames (1024 samples per frame is popular).

Let's consider a function that tells us how many frames we can expect to get from a file of a given size.

Stub function

Sketch the frame count function into framer.py:

def get_frame_count(nsamples, hop):
    """Given the number of samples, return the number of non-overlapping frames of length hop we can extract from them.""" 
    return 0

So we have a stub implementation that always returns zero.

Unit tests

Now, before filling in the details, we go and write a test for it. The reason we do this first is so that we have the opportunity to think about what we really expect our function to do -- it will turn out that we have some decisions to make.

Discussion: What tests do we need?

In test_framer.py:

import framer as fr

def test_get_frame_count():
    assert fr.get_frame_count(0, 10) == 0
    assert fr.get_frame_count(4, 2) == 2
    assert fr.get_frame_count(4, 3) == 2

etc. Note that for the last of these, we have to start thinking about what to do with partial frames -- do we zero-pad them and return them? do we return them part-full? do we skip them entirely? (I would pick the first of these myself but it depends on the application.)

This is test-driven development -- we are using the phase in which we write the tests, to solve problems about what behaviour we really want.

We run nosetests, which runs all the functions it finds called test_-something in files called test_-something in the current directory and subdirectories (recursively searched), and it fails.

(Note -- it turns out that nosetests will only pick up code from files that are not executable. On the network file system in the MAT lab, all files are executable it seems. Run nosetests --exe to include those.)

Implementation

Exercise: Implement get_frame_count to satisfy the tests.

Applying this to our marlin example

Rearranging our files to make functions testable

The above example also shows how to call code in one file from another one.

We can rearrange our marlin counting program, then, so that it is simply a module of functions we can call from elsewhere. That is, replace the main loop (the bit whose core is something like for line in source...) with a function count_marlin_in(file) that takes a filename argument and returns the number of marlin observed in that file.

Now if we run the marlin.py file, nothing happens -- it only defines functions, it doesn't actually call them any more.

But we can do e.g. at the IPython prompt,

In [12]: import marlin as m
In [13]: m.count_marlin_in("data.txt")
6

Test data and unit tests

Now we can make a set of small data files for specific tests.

Discussion: What tests do we need? e.g. empty file, no observations, observations but no marlin, some marlin, etc.

For each, we can write a simple unit test into test_marlin.py:

def test_empty_file():
    assert m.count_marlin_in("empty.txt") == 0

etc.

Exercise: Write these tests.