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 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289
|
==========
Quickstart
==========
This document should talk you through everything you need to get started with
Hypothesis.
----------
An example
----------
Suppose we've written a :wikipedia:`run length encoding <Run-length_encoding>`
system and we want to test it out.
We have the following code which I took straight from the
`Rosetta Code <https://rosettacode.org/wiki/Run-length_encoding>`_ wiki (OK, I
removed some commented out code and fixed the formatting, but there are no
functional modifications):
.. code:: python
def encode(input_string):
count = 1
prev = ""
lst = []
for character in input_string:
if character != prev:
if prev:
entry = (prev, count)
lst.append(entry)
count = 1
prev = character
else:
count += 1
entry = (character, count)
lst.append(entry)
return lst
def decode(lst):
q = ""
for character, count in lst:
q += character * count
return q
We want to write a test for this that will check some invariant of these
functions.
The invariant one tends to try when you've got this sort of encoding /
decoding is that if you encode something and then decode it then you get the same
value back.
Let's see how you'd do that with Hypothesis:
.. code:: python
from hypothesis import given
from hypothesis.strategies import text
@given(text())
def test_decode_inverts_encode(s):
assert decode(encode(s)) == s
(For this example we'll just let pytest discover and run the test. We'll cover
other ways you could have run it later).
The text function returns what Hypothesis calls a search strategy. An object
with methods that describe how to generate and simplify certain kinds of
values. The :func:`@given <hypothesis.given>` decorator then takes our test
function and turns it into a
parametrized one which, when called, will run the test function over a wide
range of matching data from that strategy.
Anyway, this test immediately finds a bug in the code:
.. code::
Falsifying example: test_decode_inverts_encode(s='')
UnboundLocalError: local variable 'character' referenced before assignment
Hypothesis correctly points out that this code is simply wrong if called on
an empty string.
If we fix that by just adding the following code to the beginning of our ``encode`` function
then Hypothesis tells us the code is correct (by doing nothing as you'd expect
a passing test to).
.. code:: python
if not input_string:
return []
If we wanted to make sure this example was always checked we could add it in
explicitly by using the :obj:`@example <hypothesis.example>` decorator:
.. code:: python
from hypothesis import example, given, strategies as st
@given(st.text())
@example("")
def test_decode_inverts_encode(s):
assert decode(encode(s)) == s
This can be useful to show other developers (or your future self) what kinds
of data are valid inputs, or to ensure that particular edge cases such as
``""`` are tested every time. It's also great for regression tests because
although Hypothesis will :ref:`remember failing examples <database>`,
we don't recommend distributing that database.
It's also worth noting that both :obj:`@example <hypothesis.example>` and
:func:`@given <hypothesis.given>` support keyword arguments as
well as positional. The following would have worked just as well:
.. code:: python
@given(s=st.text())
@example(s="")
def test_decode_inverts_encode(s):
assert decode(encode(s)) == s
Suppose we had a more interesting bug and forgot to reset the count
each time. Say we missed a line in our ``encode`` method:
.. code:: python
def encode(input_string):
count = 1
prev = ""
lst = []
for character in input_string:
if character != prev:
if prev:
entry = (prev, count)
lst.append(entry)
# count = 1 # Missing reset operation
prev = character
else:
count += 1
entry = (character, count)
lst.append(entry)
return lst
Hypothesis quickly informs us of the following example:
.. code::
Falsifying example: test_decode_inverts_encode(s='001')
Note that the example provided is really quite simple. Hypothesis doesn't just
find *any* counter-example to your tests, it knows how to simplify the examples
it finds to produce small easy to understand ones. In this case, two identical
values are enough to set the count to a number different from one, followed by
another distinct value which should have reset the count but in this case
didn't.
----------
Installing
----------
Hypothesis is :pypi:`available on PyPI as "hypothesis" <hypothesis>`. You can install it with:
.. code:: bash
pip install hypothesis
You can install the dependencies for :doc:`optional extensions <extras>` with
e.g. ``pip install hypothesis[pandas,django]``.
If you want to install directly from the source code (e.g. because you want to
make changes and install the changed version), check out the instructions in
:gh-file:`CONTRIBUTING.rst`.
-------------
Running tests
-------------
In our example above we just let pytest discover and run our tests, but we could
also have run it explicitly ourselves:
.. code:: python
if __name__ == "__main__":
test_decode_inverts_encode()
We could also have done this as a :class:`python:unittest.TestCase`:
.. code:: python
import unittest
class TestEncoding(unittest.TestCase):
@given(text())
def test_decode_inverts_encode(self, s):
self.assertEqual(decode(encode(s)), s)
if __name__ == "__main__":
unittest.main()
A detail: This works because Hypothesis ignores any arguments it hasn't been
told to provide (positional arguments start from the right), so the self
argument to the test is simply ignored and works as normal. This also means
that Hypothesis will play nicely with other ways of parameterizing tests. e.g
it works fine if you use pytest fixtures for some arguments and Hypothesis for
others.
-------------
Writing tests
-------------
A test in Hypothesis consists of two parts: A function that looks like a normal
test in your test framework of choice but with some additional arguments, and
a :func:`@given <hypothesis.given>` decorator that specifies
how to provide those arguments.
Here are some other examples of how you could use that:
.. code:: python
from hypothesis import given, strategies as st
@given(st.integers(), st.integers())
def test_ints_are_commutative(x, y):
assert x + y == y + x
@given(x=st.integers(), y=st.integers())
def test_ints_cancel(x, y):
assert (x + y) - y == x
@given(st.lists(st.integers()))
def test_reversing_twice_gives_same_list(xs):
# This will generate lists of arbitrary length (usually between 0 and
# 100 elements) whose elements are integers.
ys = list(xs)
ys.reverse()
ys.reverse()
assert xs == ys
@given(st.tuples(st.booleans(), st.text()))
def test_look_tuples_work_too(t):
# A tuple is generated as the one you provided, with the corresponding
# types in those positions.
assert len(t) == 2
assert isinstance(t[0], bool)
assert isinstance(t[1], str)
Note that as we saw in the above example you can pass arguments to :func:`@given <hypothesis.given>`
either as positional or as keywords.
--------------
Where to start
--------------
You should now know enough of the basics to write some tests for your code
using Hypothesis. The best way to learn is by doing, so go have a try.
If you're stuck for ideas for how to use this sort of test for your code, here
are some good starting points:
1. Try just calling functions with appropriate arbitrary data and see if they
crash. You may be surprised how often this works. e.g. note that the first
bug we found in the encoding example didn't even get as far as our
assertion: It crashed because it couldn't handle the data we gave it, not
because it did the wrong thing.
2. Look for duplication in your tests. Are there any cases where you're testing
the same thing with multiple different examples? Can you generalise that to
a single test using Hypothesis?
3. `This piece is designed for an F# implementation
<https://fsharpforfunandprofit.com/posts/property-based-testing-2/>`_, but
is still very good advice which you may find helps give you good ideas for
using Hypothesis.
If you have any trouble getting started, don't feel shy about
:doc:`asking for help <community>`.
|