Slide 1

Slide 1 text

Entertaining testing with pytest Roman Imankulov / @rdotpy / 21 Apr 2017

Slide 2

Slide 2 text

Why developers don’t like writing tests?

Slide 3

Slide 3 text

Testing in Python is a religion • Original sin • Absolution through pain and suffering • Mystical experience

Slide 4

Slide 4 text

Original sin ● Original sin — dynamic typing and duck typing ● As a result, a natural inclination of a Python developer to create little and stupid mistakes exposed as runtime errors

Slide 5

Slide 5 text

Absolution through pain and suffering Boilerplate Code class TestSequenceFunctions(unittest.TestCase): def setUp(self): ... def tearDown(self): ... def testFoo(self): ...

Slide 6

Slide 6 text

Absolution through pain and suffering Verbose asserts self.assertEqual(foo, 1, 'foo is not equal to one')

Slide 7

Slide 7 text

Mystical experience Django testing setups & teardowns

Slide 8

Slide 8 text

Are there any alternatives?

Slide 9

Slide 9 text

pytest

Slide 10

Slide 10 text

file: test_something.py def test_lowercase(): name = "OPOPython" assert name.lower() == "opopython"

Slide 11

Slide 11 text

No content

Slide 12

Slide 12 text

No content

Slide 13

Slide 13 text

No content

Slide 14

Slide 14 text

pytest is not a yet another xUnit framework!

Slide 15

Slide 15 text

pytest fixtures That’s what makes the difference

Slide 16

Slide 16 text

pytest fixtures Naive approach. How I’d do it myself file: fixtures.py def get_user(): return User(name='Roman', age=33, ...) file: test_user.py def test_user(): user = get_user() assert user.name == 'Roman'

Slide 17

Slide 17 text

pytest fixtures Pytest approach file: conftest.py @pytest.fixture def user(): return User(name='Roman', age=33, ...) file: test_user.py def test_user(user): assert user.name == 'Roman'

Slide 18

Slide 18 text

Fixture dependencies

Slide 19

Slide 19 text

@pytest.fixture def user(): return User(name='Roman', age=33, ...) @pytest.fixture def task(user): return Task(user=user, content='...') def test_task(task): assert task.user.name == 'Roman'

Slide 20

Slide 20 text

Fixture dependencies. Patching object @pytest.fixture def premium(user) user.set_premium() def test_premium(user, premium): assert user.is_premum()

Slide 21

Slide 21 text

yield_fixture 2 in 1, setup and teardown

Slide 22

Slide 22 text

@pytest.yield_fixture def user(): obj = User(name='Roman', age=30, ...) yield obj obj.delete()

Slide 23

Slide 23 text

Fixture scopes • function scope • module scope • session scope

Slide 24

Slide 24 text

Session fixture. Local cache @pytest.yield_fixture(scope='session', autouse=True) def local_cache(): old_settings = settings.CACHES settings.CACHES = {'default': {…}} yield settings.CACHES = old_settings

Slide 25

Slide 25 text

Function fixture. Database transaction rollback @pytest.yield_fixture def tx(): db().start_transaction() yield db().rollback() def test_user(user, tx, project, task): # project & task will be removed automatically

Slide 26

Slide 26 text

Session fixture. Clean redis @pytest.yield_fixture(scope='session') def redis_server(): proc = subp.Popen(['redis-server', '--port', 7777], ... ) yield proc proc.terminate() @pytest.fixture def rc(redis_server): client = redis.StrictRedis('redis://127.0.0.1:7777') client.flushall() return client

Slide 27

Slide 27 text

fixtures parametrization

Slide 28

Slide 28 text

Function returning a function @pytest.fixture def set_lang(user): def func(lang_code): user.set_lang(lang_code) return func def test_languages(user, set_lang): set_lang('pt') ...

Slide 29

Slide 29 text

Crazy things

Slide 30

Slide 30 text

Fixtures in a separate thread http://bit.ly/test_pool @pytest.fixture(scope='session') def item_gen(): gen = Generator(lambda: .) gen.start() return gen @pytest.yield_fixture def item(item_gen, item_rel): item = item_gen.get() yield item item_rel.put(item) @pytest.fixture(scope='session') def item_rel(): rel = Releaser(lambda o: ...) rel.start() return rel

Slide 31

Slide 31 text

More use cases for fixtures • warnings: turn MySQL warnings to errors • mock: initialize mockup objects • freezegun: time management • selenium: run a web driver

Slide 32

Slide 32 text

What else @pytest.mark.parametrize("input,expected", [ ("3+5", 8), ("2+4", 6), ]) def test_eval(input, expected): assert eval(input) == expected

Slide 33

Slide 33 text

What else def pytest_addoption(parser): parser.addoption("--clean-mysql", action="store_true", default=False) @pytest.fixture(scope='session', autouse=True) def clean_mysql(request): if not request.config.getoption(‘--clean-mysql'): return # clean MySQL tables heres

Slide 34

Slide 34 text

What else • pytest-sugar: beautiful output • pytest-django: integration with Django • pytest-xdist: parallel and distributed testing

Slide 35

Slide 35 text

What else • tox: testing against different versions of Python • detox: the same, but in parallel [tox] envlist = py27,py35,py36 [testenv] deps=pytest commands=py.test

Slide 36

Slide 36 text

Thank you! Questions? Roman Imankulov / @rdotpy