Slide 1

Slide 1 text

Let the computer write the tests Property testing with Hypothesis Dan Crosta @lazlofruvous

Slide 2

Slide 2 text

Your code is buggy (mine is too) https://www.pexels.com/photo/bug-close-up-closeup-fly-541124/

Slide 3

Slide 3 text

Property Testing https://www.pexels.com/photo/abstract-architect-architectural-design-art-323645/

Slide 4

Slide 4 text

def test_list_mean(): lst = list(range(10)) mean = list_mean(lst) # Even though we could predict the exact # mean for this simple case, when testing # with Hypothesis, we relax the assertion # so that it works for many values assert min(lst) <= mean <= max(lst) Concepts → Using Hypothesis → Custom Strategies → Other Tests You Need → Shrinking → Tidbits

Slide 5

Slide 5 text

def utf8_encode(text): return text.encode("utf8") def utf8_decode(bytes): return bytes.decode("utf8") def test_decode_encode(): # Definitely probably UTF-8 value = b"f\xc3\xbc\xc3\xb1ky d\xc3\xbc\xc3\xa7k" roundtripped = utf8_encode(utf8_decode(value)) assert roundtripped == value Concepts → Using Hypothesis → Custom Strategies → Other Tests You Need → Shrinking → Tidbits

Slide 6

Slide 6 text

@contextmanager def does_not_raise(): try: yield except Exception as e: pytest.fail("Unexpected exception") def test_it_doesnt_fail(): with does_not_raise(): my_function(123) Concepts → Using Hypothesis → Custom Strategies → Other Tests You Need → Shrinking → Tidbits

Slide 7

Slide 7 text

Using Hypothesis https://www.pexels.com/photo/person-holding-a-green-plant-1072824/

Slide 8

Slide 8 text

class MediaMetadata: def __init__(self, medium, rotation, width, height): self.medium = medium self.rotation = rotation self.width = width self.height = height def get_display_size(metadata): """Get the display width and height of a video, accounting for rotation. A video with rotation of either 90 or 270 degress returns (height, width) while a rotation of either 0 or 180 returns (width, height).""" Concepts → Using Hypothesis → Custom Strategies → Other Tests You Need → Shrinking → Tidbits

Slide 9

Slide 9 text

def get_display_size(metadata): """...""" assert metadata.medium in ("audio", "video") if metadata.medium == "video": assert metadata.width and metadata.height assert metadata.rotation % 90 == 0 if metadata.rotation % 360 in (90, 270): return (metadata.height, metadata.width) else: return (metadata.width, metadata.height) else: return None Concepts → Using Hypothesis → Custom Strategies → Other Tests You Need → Shrinking → Tidbits

Slide 10

Slide 10 text

def test_get_display_size(): # make sure rotation is handled properly metadata = MediaMetadata( medium="video", rotation=90, width=1920, height=1080, ) assert (1080, 1920) == get_display_size(metadata) Concepts → Using Hypothesis → Custom Strategies → Other Tests You Need → Shrinking → Tidbits

Slide 11

Slide 11 text

from hypothesis import given, strategies as st @given( medium=st.just("video"), rotation=st.just(90), width=st.just(1920), height=st.just(1080), ) def test_get_display_size(medium, rotation, width, height): # make sure rotation is handled properly metadata = MediaMetadata( medium, rotation, width, height, ) assert (height, width) == get_display_size(metadata) Concepts → Using Hypothesis → Custom Strategies → Other Tests You Need → Shrinking → Tidbits

Slide 12

Slide 12 text

Strategies • just() / one_of() / sampled_from() • characters() / text() / binary() / from_regex() • booleans() / integers() / floats() • lists(...) / dictionaries(...) / frozensets(...) • dates() / times() / datetimes() Concepts → Using Hypothesis → Custom Strategies → Other Tests You Need → Shrinking → Tidbits

Slide 13

Slide 13 text

from hypothesis import given, strategies as st @given( medium=st.just("video"), rotation=st.just(90), width=st.just(1920), height=st.just(1080), ) def test_get_display_size(medium, rotation, width, height): # make sure rotation is handled properly metadata = MediaMetadata( medium, rotation, width, height, ) assert (height, width) == get_display_size(metadata) Concepts → Using Hypothesis → Custom Strategies → Other Tests You Need → Shrinking → Tidbits

Slide 14

Slide 14 text

@given( medium=st.text(), rotation=st.just(90), width=st.just(1920), height=st.just(1080), ) def test_get_display_size(medium, rotation, width, height): # make sure rotation is handled properly metadata = MediaMetadata( medium, rotation, width, height, ) assert (height, width) == get_display_size(metadata) Concepts → Using Hypothesis → Custom Strategies → Other Tests You Need → Shrinking → Tidbits

Slide 15

Slide 15 text

Traceback (most recent call last): File ".../test_media.py", line 30, in test_get_display_size medium=st.text(), File ".../site-packages/hypothesis/core.py", line 960, in wrapped_test raise the_error_hypothesis_found File ".../test_media.py", line 41, in test_get_display_size assert (height, width) == get_display_size(metadata) File ".../test_media.py", line 15, in get_display_size assert metadata.medium in ("audio", "video") AssertionError: assert '' in ('audio', 'video') + where '' = .medium Falsifying example: test_get_display_size(medium='', rotation=90, width=1920, height=1080) Concepts → Using Hypothesis → Custom Strategies → Other Tests You Need → Shrinking → Tidbits

Slide 16

Slide 16 text

@given( medium=st.sampled_from(["audio", "video"]), rotation=st.just(90), width=st.just(1920), height=st.just(1080), ) def test_get_display_size(medium, rotation, width, height): # make sure rotation is handled properly metadata = MediaMetadata( medium, rotation, width, height, ) assert (height, width) == get_display_size(metadata) Concepts → Using Hypothesis → Custom Strategies → Other Tests You Need → Shrinking → Tidbits

Slide 17

Slide 17 text

Traceback (most recent call last): File ".../test_media.py", line 30, in test_get_display_size medium=st.sampled_from(["audio", "video"]), File ".../site-packages/hypothesis/core.py", line 960, in wrapped_test raise the_error_hypothesis_found File ".../test_media.py", line 41, in test_get_display_size assert (height, width) == get_display_size(metadata) AssertionError: assert (1080, 1920) == None + where None = get_display_size() Falsifying example: test_get_display_size(medium='audio', rotation=90, width=1920, height=1080) Concepts → Using Hypothesis → Custom Strategies → Other Tests You Need → Shrinking → Tidbits

Slide 18

Slide 18 text

@given( medium=st.sampled_from(["audio", "video"]), rotation=st.just(90), width=st.just(1920), height=st.just(1080), ) def test_get_display_size(medium, rotation, width, height): # make sure rotation is handled properly metadata = MediaMetadata(medium, rotation, width, height) if medium == "audio": assert get_display_size(metadata) is None else: assert get_display_size(metadata) == (height, width) Concepts → Using Hypothesis → Custom Strategies → Other Tests You Need → Shrinking → Tidbits

Slide 19

Slide 19 text

@given( medium=st.sampled_from(["audio", "video"]), rotation=st.just(90), width=st.integers(min_value=1), height=st.integers(min_value=1), ) def test_get_display_size(medium, rotation, width, height): # make sure rotation is handled properly metadata = MediaMetadata(medium, rotation, width, height) if medium == "audio": assert get_display_size(metadata) is None else: assert get_display_size(metadata) == (height, width) Concepts → Using Hypothesis → Custom Strategies → Other Tests You Need → Shrinking → Tidbits

Slide 20

Slide 20 text

@given( medium=st.sampled_from(["audio", "video"]), rotation=st.integers().map(lambda i: i * 90), width=st.integers(min_value=1), height=st.integers(min_value=1), ) def test_get_display_size(medium, rotation, width, height): # make sure rotation is handled properly metadata = MediaMetadata(medium, rotation, width, height) if medium == "audio": assert get_display_size(metadata) is None else: w, h = get_display_size(metadata) assert w, h in [(width, height), (height, width)] Concepts → Using Hypothesis → Custom Strategies → Other Tests You Need → Shrinking → Tidbits

Slide 21

Slide 21 text

Custom Strategies https://www.pexels.com/photo/architecture-blur-building-colourful-392031/

Slide 22

Slide 22 text

def mediums(): """ Generate valid mediums for MediaMetadata objects. """ return st.sampled_from(["audio", "video"]) @given( medium=mediums(), rotation=st.integers().map(lambda i: i * 90), width=st.integers(min_value=1), height=st.integers(min_value=1), ) def test_get_display_size(medium, rotation, width, height): # ... Concepts → Using Hypothesis → Custom Strategies → Other Tests You Need → Shrinking → Tidbits

Slide 23

Slide 23 text

def video_sizes(): return st.integers(min_value=1) def video_rotations(): return st.integers().map(lambda i: i * 90) @given( medium=mediums(), rotation=video_rotations(), width=video_sizes(), height=video_sizes(), ) def test_get_display_size(medium, rotation, width, height): # ... Concepts → Using Hypothesis → Custom Strategies → Other Tests You Need → Shrinking → Tidbits

Slide 24

Slide 24 text

def media_metadatas(): """ Generate valid, plausible MediaMetadata objects. """ return st.builds( MediaMetadata, medium=mediums(), rotation=video_rotations(), width=video_sizes(), height=video_sizes(), ) Concepts → Using Hypothesis → Custom Strategies → Other Tests You Need → Shrinking → Tidbits

Slide 25

Slide 25 text

@given(media_metadatas()) def test_get_display_size(metadata): # make sure rotation is handled properly if metadata.medium == "audio": assert get_display_size(metadata) is None else: w, h = get_display_size(metadata) assert w, h in [ (metadata.width, metadata.height), (metadata.height, metadata.width), ] Concepts → Using Hypothesis → Custom Strategies → Other Tests You Need → Shrinking → Tidbits

Slide 26

Slide 26 text

def media_metadatas(): """ Generate valid, plausible MediaMetadata objects. """ return st.builds( MediaMetadata, medium=mediums(), rotation=video_rotations(), width=video_sizes(), height=video_sizes(), ) Concepts → Using Hypothesis → Custom Strategies → Other Tests You Need → Shrinking → Tidbits

Slide 27

Slide 27 text

@st.composite def media_metadatas(draw): """ Generate valid, plausible MediaMetadata objects. """ medium = draw(mediums()) if medium == "video": rotation = draw(video_rotations()) width = draw(video_sizes()) height = draw(video_sizes()) else: rotation = width = height = 0 return MediaMetadata(medium, rotation, width, height) Concepts → Using Hypothesis → Custom Strategies → Other Tests You Need → Shrinking → Tidbits

Slide 28

Slide 28 text

def audio_metadatas(): return st.just(MediaMetadata("audio", 0, 0, 0)) def video_metadatas(): return st.builds( MediaMetadata, medium=st.just("video"), rotation=video_rotations(), width=video_sizes(), height=video_sizes(), ) def media_metadatas(): return st.one_of([audio_metadatas(), video_metadatas()]) Concepts → Using Hypothesis → Custom Strategies → Other Tests You Need → Shrinking → Tidbits

Slide 29

Slide 29 text

Other Tests You Need https://www.pexels.com/photo/broken-heart-love-sad-14303/

Slide 30

Slide 30 text

@given( medium=st.text().filter(lambda t: t not in ("audio", "video")), rotation=st.integers().filter(lambda i: i % 90 != 0), width=st.just(0), height=st.just(0), ) def test_get_display_size_failures(medium, rotation, width, height): metadata = MediaMetadata(medium, rotation, width, height) with pytest.raises(AssertionError): get_display_size(metadata) Concepts → Using Hypothesis → Custom Strategies → Other Tests You Need → Shrinking → Tidbits

Slide 31

Slide 31 text

@given(...) @example(medium="video", rotation=90, width=0, height=1) def test_get_display_size_failures(medium, rotation, width, height): metadata = MediaMetadata(medium, rotation, width, height) with pytest.raises(AssertionError): get_display_size(metadata) Concepts → Using Hypothesis → Custom Strategies → Other Tests You Need → Shrinking → Tidbits

Slide 32

Slide 32 text

def am_i_unlucky(num): if num == 1234: raise Exception("you are very unlucky!") @given(st.integers()) @example(1234) def test_always_fails(num): am_i_unlucky(num) @given(st.integers()) def test_never_fails(num): assume(num != 1234) am_i_unlucky(num) Concepts → Using Hypothesis → Custom Strategies → Other Tests You Need → Shrinking → Tidbits

Slide 33

Slide 33 text

def test_list_mean(): lst = list(range(10)) assert list_mean(lst) == 4.5 Concepts → Using Hypothesis → Custom Strategies → Other Tests You Need → Shrinking → Tidbits

Slide 34

Slide 34 text

Shrinking https://www.pexels.com/photo/ball-ball-shaped-blur-color-235615/

Slide 35

Slide 35 text

>>> data = os.urandom(8) >>> data b'zP\xb4\xca\r\xff{\x96' >>> struct.unpack(">Q", data) (8813743250675301270,) Concepts → Using Hypothesis → Custom Strategies → Other Tests You Need → Shrinking → Tidbits

Slide 36

Slide 36 text

>>> data = os.urandom(8) >>> data b'zP\xb4\xca\r\xff{\x96'L >>> struct.unpack(">Q", data) (8813743250675301270,) >>> struct.unpack(">Q", b'\0' + data[1:]) (22716778048093078,) >>> struct.unpack(">Q", b'\0\0' + data[2:]) (198779911240598,) # ... >>> struct.unpack(">Q", b'\0\0\0\0\0\0\0' + data[7:]) (150,) Concepts → Using Hypothesis → Custom Strategies → Other Tests You Need → Shrinking → Tidbits

Slide 37

Slide 37 text

def audio_metadatas(): return st.just(MediaMetadata("audio", 0, 0, 0)) def video_metadatas(): return st.builds( MediaMetadata, medium=st.just("video"), rotation=video_rotations(), width=video_sizes(), height=video_sizes(), ) def media_metadatas(): return st.one_of([audio_metadatas(), video_metadatas()]) Concepts → Using Hypothesis → Custom Strategies → Other Tests You Need → Shrinking → Tidbits

Slide 38

Slide 38 text

Trying example: ...(medium='\x17/ \x0c', rotation=-1941619424369918455, Trying example: ...(medium='\x17/ \x0c', rotation=-1941619424369918455, Trying example: ...(medium='///ı', rotation=-128, width=0, height=0) Trying example: ...(medium='', rotation=64, width=0, height=0) Trying example: ...(medium='', rotation=32, width=0, height=0) Trying example: ...(medium='', rotation=16, width=0, height=0) Trying example: ...(medium='', rotation=8, width=0, height=0) Trying example: ...(medium='', rotation=4, width=0, height=0) Trying example: ...(medium='', rotation=3, width=0, height=0) Trying example: ...(medium='', rotation=2, width=0, height=0) Trying example: ...(medium='', rotation=1, width=0, height=0) Falsifying example: test_get_display_size_failures(medium='', rotation=1, width=0, height=0) Concepts → Using Hypothesis → Custom Strategies → Other Tests You Need → Shrinking → Tidbits

Slide 39

Slide 39 text

Tidbits https://www.pexels.com/photo/assorted-color-food-miniature-decors-1061581/

Slide 40

Slide 40 text

from hypothesis import given, settings, Verbosity from hypothesis.strategies import integers @settings(verbosity=Verbosity.verbose) @given(integers()) def test_to_demonstrate_verbosity(an_int): # ... Concepts → Using Hypothesis → Custom Strategies → Other Tests You Need → Shrinking → Tidbits

Slide 41

Slide 41 text

# conftest.py from hypothesis import settings, Verbosity settings.register_profile( "local", verbosity=Verbosity.verbose, ) settings.register_profile( "ci", max_examples=50, perform_health_check=False, ) # pip install hypothesis-pytest # pytest --hypothesis-profile=ci ... Concepts → Using Hypothesis → Custom Strategies → Other Tests You Need → Shrinking → Tidbits

Slide 42

Slide 42 text

Thank you! Dan Crosta @lazlofruvous We're hiring! bit.ly/dbx-jobs

Slide 43

Slide 43 text

No content