File: quickstart.rst

package info (click to toggle)
python-hypothesis 3.4.2-2~bpo8%2B1
  • links: PTS, VCS
  • area: main
  • in suites: jessie-backports
  • size: 1,776 kB
  • sloc: python: 14,324; sh: 228; makefile: 158
file content (319 lines) | stat: -rw-r--r-- 9,394 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
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
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
=================
Quick start guide
=================

This document should talk you through everything you need to get started with
Hypothesis.

----------
An example
----------

Suppose we've written a `run length encoding
<http://en.wikipedia.org/wiki/Run-length_encoding>`_ system and we want to test
it out.

We have the following code which I took straight from the
`Rosetta Code <http://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
      else:
          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.

Lets 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 @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 the 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:

.. code:: python

  from hypothesis import given, example
  from hypothesis.strategies import text

  @given(text())
  @example('')
  def test_decode_inverts_encode(s):
      assert decode(encode(s)) == s

You don't have to do this, but it can be useful both for clarity purposes and
for reliably hitting hard to find examples. Also in local development
Hypothesis will just remember and reuse the examples anyway, but there's not
currently a very good workflow for sharing those in your CI.

It's also worth noting that both example and given support keyword arguments as
well as positional. The following would have worked just as well:

.. code:: python

  @given(s=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
    else:
        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.

The examples Hypothesis provides are valid Python code you can run. Any
arguments that you explicitly provide when calling the function are not
generated by Hypothesis, and if you explicitly provide *all* the arguments
Hypothesis will just call the underlying function the once rather than
running it multiple times.

----------
Installing
----------

Hypothesis is `available on pypi as "hypothesis"
<https://pypi.python.org/pypi/hypothesis>`_. You can install it with:

.. code:: bash

  pip install hypothesis

or

.. code:: bash

  easy_install hypothesis

If you want to install directly from the source code (e.g. because you want to
make changes and install the changed version) you can do this with:

.. code:: bash

  python setup.py install

You should probably run the tests first to make sure nothing is broken. You can
do this with:

.. code:: bash

  python setup.py test

Note that if they're not already installed this will try to install the test
dependencies.

You may wish to do all of this in a `virtualenv <https://virtualenv.pypa.io/en/latest/>`_. For example:

.. code:: bash

  virtualenv venv
  source venv/bin/activate
  pip install hypothesis

Will create an isolated environment for you to try hypothesis out in without
affecting your system installed packages.

-------------
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 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.core.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
    import hypothesis.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.core.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 random 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
   <http://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>`.