With Test Driven Development (TDD), the tests serve as a sort of specification for the software. This means we can separate our approach to coding into two phases : what the code should do (writing the test from the specification) and how the code should do it (writing the actual production code).
When writing your tests and code you should follow the red-green-refactor pattern.
Now it’s our turn to implement new functionality using TDD! In our case we want to implement a running average class that we can use to keep track of the best estimate of current temperature from a noisy station.
Yes, this is a bit of a contrived example, but without needing to build up hours of other functionality, it will do!
n
datapoints with our most recent observation as
the last point. (i.e. a 10 point average would use the most recent observation
and the previous 9.)n
points.def test_is_list_initialized_to_empty():
"""Test if the data list is empty after initialization."""
avg = averager()
assert avg.data == []
class averager(object):
def __init__(self):
self.data = []
def test_number_of_points_is_zero_on_initialization():
"""Test that the number of data points is zero on initialization."""
avg = averager()
assert avg.number_of_data_points == 0
class averager(object):
def __init__(self):
self.data = []
self.number_of_data_points = 0
def test_adding_datapoint_to_empty():
"""Test adding a datapoint to an empty instance."""
avg = averager()
avg.add_data(1)
assert avg.data == [1]
assert avg.number_of_data_points == 1
class averager(object):
def __init__(self):
self.data = []
self.number_of_data_points = 0
def add_data(self, datapoint):
self.data.append(datapoint)
self.number_of_data_points = 1
def test_adding_datapoint_to_partially_full():
"""Test adding a datapoint to a partially full instance."""
avg = averager()
avg.add_data(1)
avg.add_data(2)
avg.add_data(3)
assert avg.data == [1, 2, 3]
assert avg.number_of_data_points == 3
class averager(object):
def __init__(self):
self.data = []
self.number_of_data_points = 0
def add_data(self, datapoint):
self.data.append(datapoint)
self.number_of_data_points += 1
def test_remove_first_point():
"""Test removing the first point from the list."""
avg = averager(3)
avg.add_data(1)
avg.add_data(2)
avg.add_data(3)
avg.remove_first_point()
assert avg.data == [2, 3]
assert avg.number_of_data_points == 2
class averager(object):
def __init__(self):
self.data = []
self.number_of_data_points = 0
def add_data(self, datapoint):
self.data.append(datapoint)
self.number_of_data_points += 1
def remove_first_point(self):
self.data.pop(0)
self.number_of_data_points -= 1
def test_adding_datapoint_to_full():
avg = averager(3)
for i in range(1, 5):
avg.add_data(i)
assert avg.data == [2, 3, 4]
assert avg.number_of_data_points == 3
class averager(object):
def __init__(self, npts_average):
self.data = []
self.number_of_data_points = 0
self.npts_average = npts_average
def add_data(self, datapoint):
self.data.append(datapoint)
self.number_of_data_points += 1
if self.number_of_data_points > self.npts_average:
self.remove_first_point()
def remove_first_point(self):
self.data.pop(0)
self.number_of_data_points -= 1
def test_average_for_one_data_point():
avg = averager(3)
avg.add_data(5)
average = avg.running_mean()
assert average == 5
class averager(object):
def __init__(self, npts_average):
self.data = []
self.number_of_data_points = 0
self.npts_average = npts_average
def add_data(self, datapoint):
self.data.append(datapoint)
self.number_of_data_points += 1
if self.number_of_data_points > self.npts_average:
self.remove_first_point()
def remove_first_point(self):
self.data.pop(0)
self.number_of_data_points -= 1
def running_mean(self):
return 5
def test_average_for_n_minus_one_data_points():
avg = averager(3)
avg.add_data(5)
avg.add_data(7)
average = avg.running_mean()
assert average == 6
class averager(object):
def __init__(self, npts_average):
self.data = []
self.number_of_data_points = 0
self.npts_average = npts_average
def add_data(self, datapoint):
self.data.append(datapoint)
self.number_of_data_points += 1
if self.number_of_data_points > self.npts_average:
self.remove_first_point()
def remove_first_point(self):
self.data.pop(0)
self.number_of_data_points -= 1
def running_mean(self):
return sum(self.data) / self.number_of_data_points
def test_average_for_n_data_points():
avg = averager(3)
avg.add_data(5)
avg.add_data(7)
avg.add_data(9)
average = avg.running_mean()
assert average == 7
# No change to production code - be very careful here!
def test_average_for_n_plus_one_data_points():
avg = averager(3)
avg.add_data(5)
avg.add_data(7)
avg.add_data(9)
avg.add_data(11)
average = avg.running_mean()
assert average == 9
# No change again, but the expected result if this failed would be 8
def test_average_for_2n_data_points():
avg = averager(3)
for i in range(1, 7):
avg.add_data(i)
average = avg.running_mean()
assert average == 5
# No change!
Can you think of cases we missed? What about removing data from a zero length dataset? What about a zero length average?