(intro_hypothesis) jack@localhost:~/Repos/intro-to-hypothesis$ tree -L 2
.
├── my_package
│ ├── __init__.py
│ └── my_module.py
└── tests
├── __init__.py
└── test_my_module.py
# my_package/my_module.py
def add_numbers(a, b):
return a + b
# tests/test_my_module.py
from my_package.my_module import add_numbers
def test_add_numers_is_commutative():
assert add_numbers(1.23, 4.56) == add_numbers(4.56, 1.23)
pytest -v
# or...
python -m pytest -v
==================================== test session starts =====================================
platform linux -- Python 3.6.3, pytest-3.5.0, py-1.5.3, pluggy-0.6.0
-- /home/jack/.virtualenvs/intro_hypothesis/bin/python3.6
cachedir: .pytest_cache
rootdir: /home/jack/Repos/intro-to-hypothesis, inifile:
collected 1 item
tests/test_my_module.py::test_add_numers_is_commutative PASSED [100%]
================================== 1 passed in 0.01 seconds ==================================
You didn't prove that the commutative property holds in general. You just proved that such property holds for this specific case.
The combination of 1.23 and 4.56 is a very tiny subset of the entire input space of numbers that your function can receive...
# tests/test_my_module.py
from my_package.my_module import add_numbers
def test_add_numers_is_commutative():
assert add_numbers(1.23, 4.56) == add_numbers(4.56, 1.23)
# tests/test_my_module.py
from my_package.my_module import add_numbers
def test_add_numers_is_commutative():
assert add_numbers(1.23, 4.56) == add_numbers(4.56, 1.23)
def test_add_numers_is_commutative_another_case(self):
assert add_numbers(0.789, 321) == add_numbers(321, 0.789)
# more tests here...
You simply write more test cases.
For these other specific cases the commutative property holds. But you don't want to write a million test cases by hand...
# tests/test_my_module.py
import random
import unittest
from ddt import ddt, idata, unpack
from my_package.my_module import add_numbers
def float_pairs_generator():
num_test_cases = 100
for i in range(num_test_cases):
a = random.random() * 10.0
b = random.random() * 10.0
yield (a, b)
@ddt
class TestAddNumbers(unittest.TestCase):
@idata(float_pairs_generator())
@unpack
def test_add_floats_ddt(self, a, b):
self.assertEqual(add_numbers(a, b), add_numbers(b, a))
You use fuzzing to create random test cases at every run.
You use ddt to multiply the test cases (works with pytest too).
==================================== test session starts =====================================
platform linux -- Python 3.6.3, pytest-3.5.0, py-1.5.3, pluggy-0.6.0
-- /home/jack/.virtualenvs/intro_hypothesis/bin/python3.6
cachedir: .pytest_cache
rootdir: /home/jack/Repos/intro-to-hypothesis, inifile:
collected 100 items
tests/test_my_module.py::TestAddNumbers::test_add_floats_ddt_00001__1_5626962926374943__1_9960917540401857_ PASSED [ 1%]
tests/test_my_module.py::TestAddNumbers::test_add_floats_ddt_00002__8_826169800117212__0_46531523690026333_ PASSED [ 2%]
tests/test_my_module.py::TestAddNumbers::test_add_floats_ddt_00003__5_9016174415787415__9_626363288868493_ PASSED [ 3%]
tests/test_my_module.py::TestAddNumbers::test_add_floats_ddt_00004__4_2946227013991685__6_73085683629837_ PASSED [ 4%]
tests/test_my_module.py::TestAddNumbers::test_add_floats_ddt_00005__5_758774597260805__9_72994743211482_ PASSED [ 5%]
...
tests/test_my_module.py::TestAddNumbers::test_add_floats_ddt_00096__7_322612946947605__3_3536474120855377_ PASSED [ 96%]
tests/test_my_module.py::TestAddNumbers::test_add_floats_ddt_00097__1_075369396293605__4_872490525884292_ PASSED [ 97%]
tests/test_my_module.py::TestAddNumbers::test_add_floats_ddt_00098__1_5173664261532571__1_2611556220323872_ PASSED [ 98%]
tests/test_my_module.py::TestAddNumbers::test_add_floats_ddt_00099__6_727606012779317__2_4322871197800144_ PASSED [ 99%]
tests/test_my_module.py::TestAddNumbers::test_add_floats_ddt_00100__9_319277865106583__3_9858815547475537_ PASSED [100%]
================================= 100 passed in 0.15 seconds =================================
If you think about it, we are still testing some random combinations of numbers between 0.0 and 10.0 here...
It's not a very extensive portion of the input domain of the function add_numbers.
# tests/test_my_module.py
from hypothesis import given
from hypothesis.strategies import floats
from my_package.my_module import add_numbers
@given(a=floats(), b=floats())
def test_add_numbers(a, b):
assert add_numbers(a, b) == add_numbers(b, a)
Increase the verbosity level of your test by using the @settings decorator to find it out.
# tests/test_my_module.py
from hypothesis import given, settings, Verbosity
from hypothesis.strategies import floats
from my_package.my_module import add_numbers
@settings(verbosity=Verbosity.verbose)
@given(a=floats(), b=floats())
def test_add_numbers(a, b):
assert add_numbers(a, b) == add_numbers(b, a)
Trying example: test_add_numbers(a=nan, b=0.0)
Traceback (most recent call last):
[...]
File "/home/jack/Repos/intro-to-hypothesis/tests/test_my_module_hypothesis.py", line 10,
in test_add_numbers
assert add_numbers(a, b) == add_numbers(b, a)
AssertionError: assert nan == nan
+ where nan = add_numbers(nan, 0.0)
+ and nan = add_numbers(0.0, nan)
Trying example: test_add_numbers(a=6.4348585852518236e-232, b=0.0)
Trying example: test_add_numbers(a=-0.99999, b=3.402823466e+38)
Trying example: test_add_numbers(a=-inf, b=-1.175494351e-38)
Falsifying example: test_add_numbers(a=0.0, b=nan)
Some test cases are fine.
Some other ones are not.
from hypothesis import given
from hypothesis.strategies import floats
from my_package.my_module import add_numbers
@given(
a=floats(allow_nan=False, allow_infinity=False),
b=floats(allow_nan=False, allow_infinity=False))
def test_add_numbers(a, b):
assert add_numbers(a, b) == add_numbers(b, a)
The test fails because in Python nan is a valid float.
Is nan a valid input for your application? What about inf?
For example, if you are absolutely sure that add_numbers will never receive a nan or a inf as inputs, you can write a test that never generates either nan or inf.
What if add_numbers could in fact receive nan or inf as (invalid) inputs?
# my_package/my_module.py
import math
class NaNIsNotAllowed(ValueError):
pass
class InfIsNotAllowed(ValueError):
pass
def add_numbers(a, b):
if math.isnan(a) or math.isnan(b):
raise NaNIsNotAllowed('nan is not a valid input')
elif math.isinf(a) or math.isinf(b):
raise InfIsNotAllowed('inf is not a valid input')
return a + b
========================================== FAILURES ==========================================
______________________________________ test_add_numbers ______________________________________
@given(a=floats(), b=floats())
> def test_add_numbers(a, b):
E hypothesis.errors.MultipleFailures: Hypothesis found 2 distinct failures.
tests/test_my_module_hypothesis.py:7: MultipleFailures
----------------------------------------- Hypothesis -----------------------------------------
Falsifying example: test_add_numbers(a=0.0, b=nan)
Traceback (most recent call last):
[...]
raise NaNIsNotAllowed('nan is not a valid input')
my_package.my_module.NaNIsNotAllowed: nan is not a valid input
Falsifying example: test_add_numbers(a=0.0, b=inf)
Traceback (most recent call last):
[...]
raise InfIsNotAllowed('inf is not a valid input')
my_package.my_module.InfIsNotAllowed: inf is not a valid input
# tests/test_my_module.py
from hypothesis import given, reject
from hypothesis.strategies import floats
from my_package.my_module import add_numbers, NaNIsNotAllowed, InfIsNotAllowed
@given(a=floats(), b=floats())
def test_add_numbers_invalid_inputs(a, b):
try:
assert add_numbers(a, b) == add_numbers(b, a)
except (NaNIsNotAllowed, InfIsNotAllowed):
reject()
# tests/test_my_module.py
from hypothesis import given, example
from hypothesis.strategies import floats
from my_package.my_module import add_numbers
@example(a=1.23, b=4.56)
@given(
a=floats(allow_nan=False, allow_infinity=False),
b=floats(allow_nan=False, allow_infinity=False))
def test_add_numbers_explicit_example(a, b):
assert add_numbers(a, b) == add_numbers(b, a)
# tests/test_my_module.py
from hypothesis import given, example, reject
from hypothesis.strategies import floats
from my_package.my_module import add_numbers, NaNIsNotAllowed, InfIsNotAllowed
@example(a=1.23, b=4.56)
@given(
a=floats(allow_nan=False, allow_infinity=False),
b=floats(allow_nan=False, allow_infinity=False))
def test_add_numbers_explicit_example(a, b):
assert add_numbers(a, b) == add_numbers(b, a)
@given(
a=floats(allow_nan=False, allow_infinity=False),
b=floats(allow_nan=False, allow_infinity=False))
def test_add_numbers_valid_inputs(a, b):
assert add_numbers(a, b) == add_numbers(b, a)
@given(a=floats(), b=floats())
def test_add_numbers_invalid_inputs(a, b):
try:
assert add_numbers(a, b) == add_numbers(b, a)
except (NaNIsNotAllowed, InfIsNotAllowed):
reject()
==================================== test session starts =====================================
platform linux -- Python 3.6.3, pytest-3.5.0, py-1.5.3, pluggy-0.6.0
-- /home/jack/.virtualenvs/intro_hypothesis/bin/python3.6
cachedir: .pytest_cache
rootdir: /home/jack/Repos/intro-to-hypothesis, inifile:
plugins: hypothesis-3.55.1
collected 3 items
tests/test_my_module_hypothesis.py::test_add_numbers_explicit_example PASSED [ 33%]
tests/test_my_module_hypothesis.py::test_add_numbers_valid_inputs PASSED [ 66%]
tests/test_my_module_hypothesis.py::test_add_numbers_invalid_inputs PASSED [100%]
================================== 3 passed in 0.51 seconds ==================================
@jackdbd
giacomodebidda.com