So far we have been testing simple functions that take, at most, two parameters as arguments. There are no complex algorithms or logic at work, so the functions shouldn't behave differently depending on the input. The failure of these functions is down to our (I mean my) poor programming, rather than anything fundamentally complicated in their workings.
In practice, this is often not the case. Functions might require many parameters and their execution and output can vary wildly depending on the input. In many cases there might be a normal range of parameter space where the function output is easy to predict, then other regions where the behaviour can be much more complex. When writing tests it is important that you cover as many cases as possible. You should push the boundaries of your software to make sure that it works as expected across the entire range of input under which it is meant to operate. This is known as having good code coverage.
Testing extreme values is often referred to as covering edge and corner cases. Typically, edge cases test situations where one parameter is at an extreme, while corner cases test two (or more in a multidimensional problems) edge cases simultaneously. However, sometimes the definition isn't so clear. (The principle of testing unusual input holds, though.)
In this section we will make use of the provided grid
package. This provides functionality for working with cells in a two-dimensional grid, like the 4x4 one shown below. (The values in each cell indicate the (x, y)
position of the cell within the grid.)
(0,3) | (1,3) | (2,3) | (3,3) |
(0,2) | (1,2) | (2,2) | (3,2) |
(0,1) | (1,1) | (2,1) | (3,1) |
(0,0) | (1,0) | (2,0) | (3,0) |
Let's import the Cell
class from the package and see how it works.
from grid import Cell
help(Cell)
We'll now create a Cell
object that sits in the bulk of the grid and test that its neighbours are correct.
# grid/test/test_cell.py
def test_bulk():
""" Test that a cell in the bulk of the grid is correct. """
# Instantiate a cell in the bulk of a 4x4 grid.
c = Cell(2, 2, 4, 4)
# Make sure that the cell has 4 neighbours.
assert c.neighbours() == 4
# Check the coordinates of the neighbours.
assert c.left() == (1, 2)
assert c.right() == (3, 2)
assert c.up() == (2, 3)
assert c.down() == (2, 1)
Here we've instantiated a cell that sits at position (2, 2)
in a 4x4
grid. Like python, we choose to index from 0.
Now let's check the neighbours of the cell. It should have 4 neighbours: (1, 2)
to the left, (3, 2)
to the right, (2, 1)
below, and (2, 3)
above.
Let's run the unit test with pytest
.
!pytest grid/test/test_cell.py::test_bulk
Great, everything worked as expected. But that was easy, we could just work out the neighbours straight from the cell position by just adding and subtracting 1.
Now let's check a cell on the left-hand edge of the grid at position (0, 2)
. This should have 3 neighbours: one to the right, one below, and one above.
# grid/test/test_cell.py
def test_left_edge():
""" Test that a cell on the left edge of the grid is correct. """
# Instantiate a cell on the left edge of a 4x4 grid.
c = Cell(0, 2, 4, 4)
# Make sure that the cell has 3 neighbours.
assert c.neighbours() == 3
# Check the coordinates of the neighbours.
assert c.left() == None
assert c.right() == (1, 2)
assert c.up() == (0, 3)
assert c.down() == (0, 1)
!pytest grid/test/test_cell.py::test_left_edge
Fantastic, it works! The behaviour of the Cell
object was fundamentally different because of the input (we triggered a different set of conditions).
Let's now check a cell at the bottom left-corner. This should only have two neigbours: one to the right, and one above.
# grid/test/test_cell.py
def test_bottom_left_corner():
""" Test that a cell on the bottom left corner of the grid is correct. """
# Instantiate a cell at the bottom left corner of a 4x4 grid.
c = Cell(0, 0, 4, 4)
# Make sure that the cell has 2 neighbours.
assert c.neighbours() == 2
# Check the coordinates of the neighbours.
assert c.left() == None
assert c.right() == (1, 0)
assert c.up() == (0, 1)
assert c.down() == None
!pytest grid/test/test_cell.py::test_bottom_left_corner
Once again a different condition has been triggered by our change of input. Here we have tested a corner case.
So far we have been testing functions and objects in isolation, so called unit testing. However, it is likely that you will write software with multiple objects that need to work together in order to do something useful. The process of checking that different pieces of code work together as intended is often called integration testing.
The grid
module also contains a Grid
class that generates a matrix of Cell
objects and stores them internally. The user can then manipulate the cells by filling or emptying them. Let's import the class and see how it works.
from grid import Grid
help(Grid)
Let's have a play with the class.
grid = Grid(10, 10)
grid.fill(0, 0)
assert grid.nFilled() == 1
grid.fill(3, 7)
assert grid.nFilled() == 2
grid.empty(0, 0)
assert grid.nFilled() == 1
assert grid.cell(3, 7).occupied()
assert not grid.cell(0, 0).occupied()
Great, it looks like the two classes are working together as expected...
Here you'll be modifying the following files:
grid/grid.py
grid/test/test_grid.py
.