File: 2016-04-29-testing-performance-optimizations.md

package info (click to toggle)
python-hypothesis 6.138.0-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 15,272 kB
  • sloc: python: 62,853; ruby: 1,107; sh: 253; makefile: 41; javascript: 6
file content (127 lines) | stat: -rw-r--r-- 4,351 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
---
tags: technical, intro, python, properties
date: 2016-04-29 11:00
title: Testing performance optimizations
author: drmaciver
---

Once you've
[flushed out the basic crashing bugs](../getting-started-with-hypothesis/)
in your code, you're going to want to look for more interesting things to test.

The next easiest thing to test is code where you know what the right answer is for every input.

Obviously in theory you think you know what the right answer is - you can just run the code. That's not very
helpful though, as that's the answer you're trying to verify.

But sometimes there is more than one way to get the right answer, and you choose the one you run in production
not because it gives a different answer but because it gives the same answer *faster*.

<!--more-->

For example:

* There might be a fancy but fast version of an algorithm and a simple but slow version of an algorithm.
* You might have a caching layer and be able to run the code with and without caching turned on, or with a
  different cache timeout.
* You might be moving to a new database backend to improve your scalability, but you still have the code for
  the old backend until you've completed your migration.

There are plenty of other ways this can crop up, but those are the ones that seem the most common.

Anyway, this creates an *excellent* use case for property based testing, because if two functions are supposed
to always return the same answer, you can test that: Just call both functions with the same data and assert
that their answer is the same.

Lets look at this in the fancy algorithm case. Suppose we implemented [merge sort](
https://en.wikipedia.org/wiki/Merge_sort):

```python
def merge_sort(ls):
    if len(ls) <= 1:
        return ls
    else:
        k = len(ls) // 2
        return merge_sorted_lists(merge_sort(ls[:k]), merge_sort(ls[k:]))


def merge_sorted_lists(x, y):
    result = []
    i = 0
    j = 0
    while i < len(x) and j < len(y):
        if x[i] <= y[j]:
            result.append(x[i])
            i += 1
        else:
            result.append(y[j])
            j += 1
    return result
```

We want a reference implementation to test it against, so lets also implement [bubble sort](
https://en.wikipedia.org/wiki/Bubble_sort):

```python
def bubble_sort(ls):
    ls = list(ls)
    needs_sorting = True
    while needs_sorting:
        needs_sorting = False
        for i in range(1, len(ls)):
            if ls[i - 1] > ls[i]:
                needs_sorting = True
                ls[i - 1], ls[i] = ls[i], ls[i - 1]
    return ls
```

These *should* always give the same answer,  so lets test that:

```python
@given(lists(integers()))
def test_bubble_sorting_is_same_as_merge_sorting(ls):
    assert bubble_sort(ls) == merge_sort(ls)
```

This gives us an error:

```
    @given(lists(integers()))
    def test_bubble_sorting_is_same_as_merge_sorting(ls):
>       assert bubble_sort(ls) == merge_sort(ls)
E       assert [0, 0] == [0]
E         Left contains more items, first extra item: 0
E         Use -v to get the full diff

foo.py:43: AssertionError
----- Hypothesis -----
Falsifying example: test_bubble_sorting_is_same_as_merge_sorting(ls=[0, 0])
```

What's happened is that we messed up our implementation of merge\_sorted\_lists, because we forgot
to include the elements left over in the other list once we've reached the end of one of them. As a
result we ended up losing elements from the list, a problem that our simpler implementation lacks.
We can fix this as follows and then the test passes:

```python
def merge_sorted_lists(x, y):
    result = []
    i = 0
    j = 0
    while i < len(x) and j < len(y):
        if x[i] <= y[j]:
            result.append(x[i])
            i += 1
        else:
            result.append(y[j])
            j += 1
    result.extend(x[i:])
    result.extend(y[j:])
    return result
```

This technique combines especially well with
[Hypothesis's stateful testing](../rule-based-stateful-testing/), because
you can use it to then test different implementations of complex APIs. For example, Hypothesis uses this
property together with stateful testing to [verify that the different implementations of its example database
behave identically](https://github.com/HypothesisWorks/hypothesis/blob/master/hypothesis-python/tests/nocover/test_database_agreement.py).