# Hypothesis Testing with Oracle Functions

*This post is about the Hypothesis testing library. If you’re unfamiliar with it, check it out! Property tests are a fantastic addition to your testing suite. All examples use pytest.*

Imagine we’ve written a bubblesort:

```
def bubblesort(l: List[int]) -> List[int]:
# blah blah blah
```

What are some of the properties we could test? We could check that the input has the same length as the output:

```
from hypothesis import given
from hypothesis.strategies import lists, integers
@given(lists(integers()))
def test_same_length(l):
assert len(bubblesort(l)) == len(l)
```

Or we could test that the sort is idempotent:

```
@given(lists(integers()))
def test_idempotent(l):
assert bubblesort(bubblesort(l)) == bubblesort(l)
```

But neither test really gets at what we want: confirming the list is actually *sorted*. Fortunately, though, there’s a simple hypothesis test for that:

```
@given(lists(integers()))
def test_sorts(l):
assert bubblesort(l) == sorted(l)
```

We just need to check that the output of the function matches the output of a sorting function! In this case, `sorted`

is an *oracle*, something we know is correct and can compare our solution to. This may look like cheating (*“if we already have a sorting function, why would we be writing another?”*), but this can be a surprisingly powerful testing strategy. Here are some common use-cases:

### Refactoring with Oracles

One use case for an oracle is when you refactor an existing function: you want to clean up or optimize something you’re confident is correct and want to make sure your changes don’t break anything. Let’s take a toy example:

```
def is_prime(x: int) -> bool:
if x < 2:
return False
for i in range(2, x):
if x % i == 0:
return False
return True
```

A common optimization we can do is only check up to the square root of the number:

```
from math import ceil, sqrt
def new_is_prime(x: int) -> bool:
# blah blah blah
for i in range(2, ceil(sqrt(x))):
# sup
```

This seems like it works for simple test cases, but if we compare it to our original function, we find an error:

```
@given(integers())
def test_matches_oracle(x):
assert is_prime(x) == new_is_prime(x)
```

This error comes from how `range`

works: `list(range(2, 2)) == []`

, so our function fails for 4. We found this by using our original version as an oracle against the refactored version.

### Special-Case Oracles

Sometimes we know our function works in specific cases but does not generalize. For example, the following function ostensibly takes a list and returns the highest possible product of three elements:

```
# We'll assume that all inputs will have more than 3 elements
# So no need for a bounds check
def buggy_max_triplet_prod(l: List[int]) -> int:
a, b, c = sorted(l, reverse=True)[0:3]
return a * b * c
```

This is correct when all of the elements are positive. However, it fails for `[-4, -3, 1, 2]`

, returning -6 instead of 12. We can still use this as an oracle, though. Since we know it’s correct for certain lists, our final function must match its output for those lists.

```
@given(lists(integers(min_value=0), min_size=3)) # only positive numbers
def test_matches_on_positive(l):
assert max_triplet_prod(l) == buggy_max_triplet_prod(l)
```

Over our restricted domain, the broken function is correct, and can be used as an oracle.

### Inefficient Oracles

Sometimes there’s a solution to a problem that we know for sure works but is too slow/memory-intensive/etc for production code. We can use that solution as an oracle to test a more sophisticated function. If you wanted to write a function to insert into a sorted array, you could use the following as a reference oracle:

```
def ordered_insert_oracle(new_element: int, ordered_list: List[int]) -> List[int]:
return sorted(ordered_list + [new_element])
@given(lists(integers), integers())
def test_ordered_insert(l, x):
assert ordered_insert(l, x) == ordered_insert_oracle(l, x)
```

The oracle will be O(n*log(n)), while a well written ordered insert will be O(n). That might matter in production, but it won’t matter too much in testing, so we can benefit from an oracle here.

### Reverse Oracles

Sometimes we can go the other way: instead of writing an oracle that will always get the right answer, we can write a function that takes an answer and finds a question for it. This gets a bit complicated, so let’s walk through an example. We’re writing our own counter.

```
from random import shuffle
@given(lists(tuples(integers(), integers(min_value=1, max_value=10)), unique_by=lambda x: x[0]))
def test_counter(l):
test_list = []
for val, times in l:
test_list += [val] * times
shuffle(test_list)
assert count(test_list) == {val:times for val, times in l}
```

Here, we’re taking a possible answer and *generating* a list that matches that. For example, if our test generates `l = [(5, 2), (1, 1)]`

, that means we want our function to return `{5: 2, 1: 1}`

. So we find a list that, *if count is written correctly*, would match that output. Our implementation is to first create the list

`[5, 5, 1]`

and then shuffle it.This test ends up being more complicated than the others because it’s easier for the test itself to have bugs. Note the `unique_by`

; that’s because our test will bug if we generate `l = [(1, 1), (1, 1)]`

: we’ll assert `{1: 2} == {1: 1}`

.

## Caveats

Most of these caveats apply to property-based tests (PBTs) in general.

- You’ll want to write some unit tests to sanity-check your oracles. This throws you down a rabbit hole (
*“I have to test my tests?!”*), but you get much wider test coverage with PBTs in return for the extra complexity. - Sometimes coming up with the oracles can be as or more difficult than writing the final functions. As always, writing good code involves tradeoffs.
- The further you get from pure functions the harder writing PBTs gets. Reverse oracles help a bit here.
- When writing bullet points like this I’m always tempted to add something completely unrelated. Here’s something I wrote about how much I like dogs.
- Don’t try to be overly clever and use oracles in cases where more basic PBTs (or even unit tests) suffice.
- The benefits PBT provides are orthogonal to the benefits you get from unit tests, type systems, formal methods, and getting a full night’s sleep. Ideally you’d want to have all five.
- Ruby doesn’t have a good PBT library and that makes me sad.