Software Testing and Testing Automation with Python

Test Driven Development Introduction

Purpose

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).

Advantages

Red-Green-Refactor

When writing your tests and code you should follow the red-green-refactor pattern.

Workflow

Bob Martin’s Three Laws of TDD

  1. You are not allowed to write any production code unless it is to make a failing unit test pass.
  2. You are not allowed to write any more of a unit test than is sufficient to fail; and compilation failures are failures.
  3. You are not allowed to write any more production code than is sufficient to pass the one failing unit test. You can read more of Uncle Bob’s explanation here.

Let’s try it

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!

Requirements

Make a test list

Start going through the list

test_is_list_initialized_to_empty

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 = []

test_number_of_points_is_zero_on_initialization

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

test_adding_datapoint_to_empty

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

test_adding_datapoint_to_partially_full

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

test_remove_first_point

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

test_adding_datapoint_to_full

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

test_average_for_one_data_point

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

test_average_for_n_minus_one_data_points

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

test_average_for_n_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!

test_average_for_n_plus_one_data_points

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

test_average_for_2n_data_points

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!

What did we miss?

Can you think of cases we missed? What about removing data from a zero length dataset? What about a zero length average?

Home