Unit Tests
- A quick review of pytest.
- Deciding what tests to write.
- Creating and using mock objects.
- Making “random” reproducible.
- Using coverage to determine what is and isn’t being tested.
Terms defined: mock object, parameterize (a test)
False Starts
- Use
GridList
andGridArray
from Chapter 4 test_grid_start.py
tests that grids can be initialized- But we don’t know if we’re getting the actual values from the grid because they’re random
- And repeating the test for different classes is error-prone as well as annoying
def test_grid_array_constructed_correctly():
g = GridArray(2, 3, 4)
assert g.width() == 2
assert g.height() == 3
assert g.depth() == 4
for x in range(g.width()):
for y in range(g.height()):
assert g[x, y] > 0
def test_grid_list_constructed_correctly():
g = GridList(2, 3, 4)
assert g.width() == 2
assert g.height() == 3
assert g.depth() == 4
for x in range(g.width()):
for y in range(g.height()):
assert g[x, y] > 0
- Create a new class
GridListRandomizer
that takes a number generator as a constructor parameter- Generate a grid filled with known values for testing
def __init__(self, width, height, depth, rand=random.randint):
"""Construct and fill."""
super().__init__(width, height, depth)
self._rand = rand
self._grid = []
for x in range(self._width):
row = []
for y in range(self._height):
row.append(self._rand(1, depth))
self._grid.append(row)
- Test looks better
def test_grid_list_with_randomizer_function():
def r(low, high):
return 12345
g = GridListRandomizer(2, 3, 4, r)
assert g.width() == 2
assert g.height() == 3
assert g.depth() == 4
for x in range(g.width()):
for y in range(g.height()):
assert g[x, y] == 12345
- But we’re no longer testing our actual grid class
- Could add extra arguments for all sorts of things to all our classes, but that’s a lot of work
Better Tools
test_grid_mock.py
replaces the random number generator with a mock object without modifying the grid class
def test_grid_list_patching_randomization():
with patch('random.randint', return_value=12345):
g = GridList(2, 3, 4)
assert g.width() == 2
assert g.height() == 3
assert g.depth() == 4
for x in range(g.width()):
for y in range(g.height()):
assert g[x, y] == 12345
test_grid_parametrize.py
parameterizes the test across both classes
@pytest.mark.parametrize('cls', [GridArray, GridList])
def test_grid_list_parameterizing_classes(cls):
with patch('random.randint', return_value=12345):
g = cls(2, 3, 4)
assert g.width() == 2
assert g.height() == 3
assert g.depth() == 4
for x in range(g.width()):
for y in range(g.height()):
assert g[x, y] == 12345
A Testable Grid
grid_filled.py
definesGridFilled
, which we can populate with whatever data we want
def __init__(self, width, height, depth, values):
"""Construct and fill."""
assert len(values) == width
assert all((len(col) == height for col in values))
super().__init__(width, height, depth)
self._grid = [col[:] for col in values]
test_grid_filled.py
starts by testing that filling from specified works correctly
def test_explicit_filling_fills_correctly():
g = GridFilled(3, 3, 4, [[1, 1, 1], [2, 2, 2], [3, 3, 3]])
assert g.width() == 3
assert g.height() == 3
assert g.depth() == 4
for x in range(g.width()):
assert all((g[x, y] == x + 1 for y in range(g.height())))
- Add test for filling grid by creating deterministic filling path
def test_filling_with_straight_run_to_edge():
g = GridFilled(3, 3, 4, [[4, 1, 4], [4, 4, 4], [4, 4, 4]])
g.fill()
assert g == GridFilled(3, 3, 4, [[4, 0, 4], [4, 0, 4], [4, 4, 4]])
- But suddenly realize: what happens when several fillable cells have the same value?
fill_grid
always chooses the first one it encounters in this case- So filling has a bias toward the (0,0) corner of the grid
Exercises
- Refactor grid classes so that we have a patchable method for filling initial values.