Test-driven Development (TDD) takes the workflow of writing code and writing tests and turns it on its head. TDD is a software development process where you write the tests first. Before you write a single line of a function, you first write the test for that function.
After you write a test, you are then allowed to proceed to write the function that you are testing. However, you are only supposed to implement enough of the function so that the test passes. If the function does not do what is needed, you write another test and then go back and modify the function. You repeat this process of test-then-implement until the function is completely implemented for your current needs.
The most important takeaway from test-driven development is that the moment you start writing code, you should be considering how to test that code. The tests should be written and presented in tandem with the implementation. Testing is too important to be an afterthought.
The following example illustrates classic (if ludicrous) TDD for a standard deviation function, std()
.
To start, we write a test for computing the standard deviation from a list of numbers as follows:
from mod import std
def test_std1():
obs = std([0.0, 2.0])
exp = 1.0
assert obs == exp
Next, we write the minimal version of std()
that will cause test_std1()
to pass:
def std(vals):
# surely this is cheating...
return 1.0
As you can see, the minimal version simply returns the expected result for the sole case that we are testing. If we only ever want to take the standard deviation of the numbers 0.0 and 2.0, or 1.0 and 3.0, and so on, then this implementation will work perfectly. If we want to branch out, then we probably need to write more robust code. However, before we can write more code, we first need to add another test or two:
def test_std1():
obs = std([0.0, 2.0])
exp = 1.0
assert_equal(obs, exp)
def test_std2():
# Test the fiducial case when we pass in an empty list.
obs = std([])
exp = 0.0
assert_equal(obs, exp)
def test_std3():
# Test a real case where the answer is not one.
obs = std([0.0, 4.0])
exp = 2.0
assert_equal(obs, exp)
A simple function implementation that would make these tests pass could be as follows:
def std(vals):
# a little better
if len(vals) == 0: # Special case the empty list.
return 0.0
return vals[-1] / 2.0 # By being clever, we can get away without doing real work.
Are we done? No. Of course not. Even though the tests all pass, this is clearly still not a generic standard deviation function. To create a better implementation, TDD states that we again need to expand the test suite:
def test_std1():
obs = std([0.0, 2.0])
exp = 1.0
assert_equal(obs, exp)
def test_std2():
obs = std([])
exp = 0.0
assert_equal(obs, exp)
def test_std3():
obs = std([0.0, 4.0])
exp = 2.0
assert_equal(obs, exp)
def test_std4():
# The first value is not zero.
obs = std([1.0, 3.0])
exp = 1.0
assert_equal(obs, exp)
def test_std5():
# Here, we have more than two values, but all of the values are the same.
obs = std([1.0, 1.0, 1.0])
exp = 0.0
assert_equal(obs, exp)
At this point, we may as well try to implement a generic standard deviation function. Recall:
$$ \sigma = \sqrt{ \frac{\sum_{\mathrm{i}} (x_{\mathrm{i}} - \bar{x})^2 }{N}}, $$where $\sigma$ is the standard deviation, $x_{\mathrm{i}}$ are the values in the sample data, $\bar{x}$ is the mean of the values and $N$ is the numver of values.
We would spend more time trying to come up with clever approximations to the standard deviation than we would spend actually coding it.
test_std.py
mod.py
It is important to note that we could improve this function by writing further tests. For example, this std()
ignores the situation where infinity is an element of the values list. There is always more that can be tested. TDD prevents you from going overboard by telling you to stop testing when you have achieved all of your use cases.