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
|
Adapting strategies
===================
This page discusses ways to adapt strategies to your needs, either by transforming them inline with |.map|, or filtering out unwanted inputs with |.filter| and |assume|.
Mapping strategy inputs
-----------------------
Sometimes you want to apply a simple transformation to a strategy. For instance, we know that we can generate lists of integers with ``lists(integers())``. But maybe we wanted to instead generate sorted lists. We could use an inline |.map| to achieve this:
.. code-block:: pycon
>>> lists(integers()).map(sorted).example()
[-25527, -24245, -93, -70, -7, 0, 39, 65, 112, 6189, 19469, 32526, 1566924430]
In general, ``strategy.map(f)`` returns a new strategy which transforms all the examples generated by ``strategy`` by calling ``f`` on them.
Filtering strategy inputs
-------------------------
Many strategies in Hypothesis offer some control over the kinds of values that get generated. For instance, ``integers(min_value=0)`` generates positive integers, and ``integers(100, 200)`` generates integers between ``100`` and ``200``.
Sometimes, you need more control than this. The inputs from a strategy may not match exactly what you need, and you just need to filter out a few bad cases.
For instance, suppose we have written a simple test involving the modulo operator ``%``:
.. code-block:: python
from hypothesis import given, strategies as st
@given(st.integers(), st.integers())
def test_remainder_magnitude(a, b):
# the remainder after division is always less than
# the divisor
assert abs(a % b) < abs(b)
Hypothesis will quickly report a failure for this test: ``ZeroDivisionError: integer modulo by zero``. Just like division, modulo isn't defined for 0. The case of ``b == 0`` isn't interesting for the test, and we would like to get rid of it.
The best way to do this is with the |.filter| method:
.. code-block:: python
from hypothesis import assume, given, strategies as st
@given(st.integers(), st.integers().filter(lambda n: n != 0))
def test_remainder_magnitude(a, b):
# b is guaranteed to be nonzero here, thanks to the filter
assert abs(a % b) < abs(b)
This test now passes cleanly.
Calling |.filter| on a strategy creates a new strategy with that filter applied at generation-time. For instance, ``integers().filter(lambda n: n != 0)`` is a strategy which generates nonzero integers.
Assuming away test cases
------------------------
|.filter| lets you filter test inputs from a single strategy. Hypothesis also provides an |assume| function for when you need to filter an entire test case, based on an arbitrary condition.
The |assume| function skips test cases where some condition evaluates to ``True``. You can use it anywhere in your test. We could have expressed our |.filter| example above using |assume| as well:
.. code-block:: python
from hypothesis import assume, given, strategies as st
@given(st.integers(), st.integers())
def test_remainder_magnitude(a, b):
assume(b != 0)
# b will be nonzero here
assert abs(a % b) < abs(b)
|assume| vs |.filter|
~~~~~~~~~~~~~~~~~~~~~
Where possible, you should use |.filter|. Hypothesis can often rewrite simple filters into more efficient sampling methods than rejection sampling, and will retry filters several times instead of aborting the entire test case (as with |assume|).
For more complex relationships that can't be expressed with |.filter|, use |assume|.
Here's an example of a test where we want to filter out two different types of examples:
.. code-block:: python
from hypothesis import assume, given, strategies as st
@given(st.integers(), st.integers())
def test_floor_division_lossless_when_b_divides_a(a, b):
# we want to assume that:
# * b is nonzero, and
# * b divides a
assert (a // b) * b == a
We could start by using |assume| for both:
.. code-block:: python
from hypothesis import assume, given, strategies as st
@given(st.integers(), st.integers())
def test_floor_division_lossless_when_b_divides_a(a, b):
assume(b != 0)
assume(a % b == 0)
assert (a // b) * b == a
And then notice that the ``b != 0`` condition can be moved into the strategy definition as a |.filter| call:
.. code-block:: python
from hypothesis import assume, given, strategies as st
@given(st.integers(), st.integers().filter(lambda n: n != 0))
def test_floor_division_lossless_when_b_divides_a(a, b):
assume(a % b == 0)
assert (a // b) * b == a
However, the ``a % b == 0`` condition has to stay as an |assume|, because it expresses a more complicated relationship between ``a`` and ``b``.
|assume| vs early-returning
~~~~~~~~~~~~~~~~~~~~~~~~~~~
One other way we could have avoided the divide-by-zero error inside the test function is to early-return when ``b == 0``:
.. code-block:: python
from hypothesis import assume, given, strategies as st
@given(st.integers(), st.integers())
def test_remainder_magnitude(a, b):
if b == 0:
# bad plan - test "passes" without checking anything!
return
assert abs(a % b) < abs(b)
While this would have avoided the divide-by-zero, early-returning is not the same as using |assume|. With |assume|, Hypothesis knows that a test case has been filtered out, and will not count it towards the |max_examples| limit. In contrast, early-returns are counted as a passing test, even though the assertions didn't run! In more complicted cases, this could end up testing your code less than you expect, because many test cases get discarded without Hypothesis knowing about it.
In addition, |assume| lets you skip the test case at any point in the test, even inside arbitrarily deep nestings of functions.
|