File: introduction.rst

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 (205 lines) | stat: -rw-r--r-- 7,780 bytes parent folder | download
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
Introduction to Hypothesis
==========================

This page introduces two fundamental parts of Hypothesis (|@given|, and strategies) and shows how to test a selection sort implementation using Hypothesis.

Install Hypothesis
------------------

First, let's install Hypothesis:

.. code-block:: shell

    pip install hypothesis

Defining a simple test
----------------------

Hypothesis tests are defined using two things; |@given|, and a *strategy*, which is passed to |@given|. Here's a simple example:

.. code-block:: python

    from hypothesis import given, strategies as st

    @given(st.integers())
    def test_is_integer(n):
        assert isinstance(n, int)

Adding the |@given| decorator turns this function into a Hypothesis test. Passing |st.integers| to |@given| says that Hypothesis should generate random integers for the argument ``n`` when testing.

We can run this test by calling it:

.. code-block:: python

    from hypothesis import given, strategies as st

    @given(st.integers())
    def test_is_integer(n):
        print(f"called with {n}")
        assert isinstance(n, int)

    test_is_integer()

Note that we don't pass anything for ``n``; Hypothesis handles generating that value for us. The resulting output looks like this:

.. code-block:: none

    called with 0
    called with -18588
    called with -672780074
    called with 32616
    ...


Testing a sorting algorithm
---------------------------

Suppose we have implemented a simple selection sort:

.. code-block:: python

  # contents of example.py
  from hypothesis import given, strategies as st

  def selection_sort(lst):
      result = []
      while lst:
          smallest = min(lst)
          result.append(smallest)
          lst.remove(smallest)
      return result

and want to make sure it's correct. We can write the following test by combining the |st.integers| and |st.lists| strategies:

.. code-block:: python

  ...

  @given(st.lists(st.integers()))
  def test_sort_correct(lst):
      print(f"called with {lst}")
      assert selection_sort(lst.copy()) == sorted(lst)

  test_sort_correct()

When running ``test_sort_correct``, Hypothesis uses the ``lists(integers())`` strategy to generate random lists of integers. Feel free to run ``python example.py`` to get an idea of the kinds of lists Hypothesis generates (and to convince yourself that this test passes).

Adding floats to our test
~~~~~~~~~~~~~~~~~~~~~~~~~

This test is a good start. But ``selection_sort`` should be able to sort lists with floats, too. If we wanted to generate lists of either integers or floats, we can change our strategy:

.. code-block:: python

  # changes to example.py
  @given(st.lists(st.integers() | st.floats()))
  def test_sort_correct(lst):
      pass

The pipe operator ``|`` takes two strategies, and returns a new strategy which generates values from either of its strategies. So the strategy ``integers() | floats()`` can generate either an integer, or a float.

.. note::

  ``strategy1 | strategy2`` is equivalent to :func:`st.one_of(strategy1, strategy2) <hypothesis.strategies.one_of>`.

Preventing |st.floats| from generating ``nan``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Even though ``test_sort_correct`` passed when we used lists of integers, it actually fails now that we've added floats! If you run ``python example.py``, you'll likely (but not always; this is random testing, after all) find that Hypothesis reports a counterexample to ``test_sort_correct``. For me, that counterexample is ``[1.0, nan, 0]``. It might be different for you.

The issue is that sorting in the presence of ``nan`` is not well defined. As a result, we may decide that we don't want to generate them while testing. We can pass ``floats(allow_nan=False)`` to tell Hypothesis not to generate ``nan``:

.. code-block:: python

  # changes to example.py
  @given(st.lists(st.integers() | st.floats(allow_nan=False)))
  def test_sort_correct(lst):
      pass

And now this test passes without issues.

.. note::

  You can use the |.example()| method to get an idea of the kinds of things a strategy will generate:

  .. code-block:: pycon

    >>> st.lists(st.integers() | st.floats(allow_nan=False)).example()
    [-5.969063e-08, 15283673678, 18717, -inf]

  Note that |.example()| is intended for interactive use only (i.e., in a :term:`REPL <python:REPL>`). It is not intended to be used inside tests.


Tests with multiple arguments
-----------------------------

If we wanted to pass multiple arguments to a test, we can do this by passing multiple strategies to |@given|:

.. code-block:: python

    from hypothesis import given, strategies as st

    @given(st.integers(), st.lists(st.floats()))
    def test_multiple_arguments(n, lst):
        assert isinstance(n, int)
        assert isinstance(lst, list)
        for f in lst:
            assert isinstance(f, float)

Keyword arguments
~~~~~~~~~~~~~~~~~

We can also pass strategies using keyword arguments:

.. code-block:: python

    @given(lst=st.lists(st.floats()), n=st.integers())  #  <-- changed
    def test_multiple_arguments(n, lst):
        pass

Note that even though we changed the order the parameters to |@given| appear, we also explicitly told it which parameters to pass to by using keyword arguments, so the meaning of the test hasn't changed.

In general, you can think of positional and keyword arguments to |@given| as being forwarded to the test arguments.

.. note::

  One exception is that |@given| does not support mixing positional and keyword arguments. See the |@given| documentation for more about how it handles arguments.


Running Hypothesis tests
------------------------

There are a few ways to run a Hypothesis test.

* Explicitly call it, like ``test_is_integer()``, as we've seen. Hypothesis tests are just normal functions, except |@given| handles generating and passing values for the function arguments.
* Let a test runner such as :pypi:`pytest` pick up on it (as long as the function name starts with ``test_``).

Concretely, when running a Hypothesis test, Hypothesis will:

* generate 100 random inputs,
* run the body of the function for each input, and
* report any exceptions that get raised.

.. note::

  The number of examples can be controlled with the |max_examples| setting. The default is 100.


When to use Hypothesis and property-based testing
-------------------------------------------------

Property-based testing is a powerful *addition* to unit testing. It is not always a replacement.

If you're having trouble coming up with a property in your code to test, we recommend trying the following:

* Look for round-trip properties: encode/decode, serialize/deserialize, etc. These property-based tests tend to be both powerful and easy to write.
* Look for ``@pytest.mark.parametrize`` in your existing tests. This is sometimes a good hint you can replace the parametrization with a strategy. For instance, ``@pytest.mark.parametrize("n", range(0, 100))`` could be replaced by ``@given(st.integers(0, 100 - 1))``.
* Simply call your code with random inputs (of the correct shape) from Hypothesis! You might be surprised how often this finds crashes. This can be especially valuable for projects with a single entrypoint interface to a lot of underlying code.

Other examples of properties include:

* An optimized implementation is equivalent to a slower, but clearly correct, implementation.
* A sequence of transactions in a financial system always "balances"; money never gets lost.
* The derivative of a polynomial of order ``n`` has order ``n - 1``.
* A type-checker, linter, formatter, or compiler does not crash when called on syntactically valid code.
* `And more <https://fsharpforfunandprofit.com/posts/property-based-testing-2/>`_.