2.4 Introduction to Property-Based Testing#
Hypothesis is a Python testing library that we’ll use occasionally in this course for exercises and assignments. It’s already available on the Teaching Lab machines, and you should have installed it on your own computer when you went through the steps of the Software Guide on Quercus.
When writing tests, we often try to identify key properties on the inputs to the function being tested. We then pick representative inputs that meet these properties, and use these inputs to write tests.
We can extend this idea to trying to identify key properties of the function itself:
central relationships between their inputs and outputs that must hold for all possible inputs.
This type of testing is called property-based testing,
and the most famous implementation of this type of testing in Python is the hypothesis
library.
An example#
Let’s see a concrete example of what these property tests might look like. Consider the following function:
def insert_after(lst: list[int], n1: int, n2: int) -> None:
"""After each occurrence of <n1> in <lst>, insert <n2>.
>>> lst = [5, 1, 2, 1, 6]
>>> insert_after(lst, 1, 99)
>>> lst
[5, 1, 99, 2, 1, 99, 6]
"""
We’ll test two properties of this function, which should hold for any valid input:
insert_after
always returnsNone
.insert_after
increases the length oflst
by the number of times thatn1
occurs in that list.
Our first test is the following:
from hypothesis import given
from hypothesis.strategies import integers, lists
from insert import insert_after
@given(lists(integers()), integers(), integers())
def test_returns_none(lst: list[int], n1: int, n2: int) -> None:
"""Test that insert_after always returns None.
"""
assert insert_after(lst, n1, n2) is None
The test case (test_returns_none
) is preceded by the line @given(lists(integers()), integers(), integers())
;
what this line does is tell hypothesis
to generate “random” inputs of the given types: a list of integers, and then two other integers.
These values are then passed to the test function,
which then simply calls insert_after
on them, and checks that the output is None
.
The most interesting part is that the “given
” line doesn’t just generate one set of random inputs; instead, it generates dozens of them (or even hundreds, depending on how hypothesis is configured), and runs this test function on each one!
We call the input specifiers like integers()
or lists()
a strategy;
we’ll see more examples of strategies throughout the term.
A more complex property#
Even though the previous test looked pretty straight-forward, don’t be fooled! Since a property test is just a Python function, we can write pretty complex tests using all of our Python knowledge.
For example, to test the second property we mentioned, we’ll need to store both the original length of lst
, and the number of times that n1
appeared in it:
@given(lists(integers()), integers(), integers())
def test_new_item_count(lst: list[int], n1: int, n2: int) -> None:
"""Test that the correct number of items is added.
"""
num_n1_occurrences = lst.count(n1)
original_length = len(lst)
insert_after(lst, n1, n2)
final_length = len(lst)
assert final_length - original_length == num_n1_occurrences
Further reading#
Hypothesis is a powerful property-based testing library, and we’re only scratching the surface of it here. If you’d like more information, please consult the official Hypothesis documentation.