Slide 1

Slide 1 text

Improve Your Testing With Pytest and Mock Pycon SG - @gabehollombe June 19, 2015

Slide 2

Slide 2 text

Hello!

Slide 3

Slide 3 text

Helps you write better programs “ ”

Slide 4

Slide 4 text

It's popular • PyPy - with over 21,000 tests • Sentry • tox • Cloudera • Spotify • many more

Slide 5

Slide 5 text

Pytest's Key Benefits • Popular • Concise • Helpful assertion messages • Powerful fixtures • Many plugins

Slide 6

Slide 6 text

Also… • runs on Posix/Windows, Python 2.6-3.4, PyPy • well tested with more than a thousand tests against itself • strict backward compatibility policy for safe upgrades • comprehensive online and PDF documentation • many third party plugins and builtin helpers • used in many small and large projects and organisations

Slide 7

Slide 7 text

Installation pip install -U pytest py.test —-version

Slide 8

Slide 8 text

Our First Test

Slide 9

Slide 9 text

def plusone(x): return x + 1 def test_answer(): assert plusone(3) == 5

Slide 10

Slide 10 text

py.test py.test py.test # from within a dir Running py.test

Slide 11

Slide 11 text

$ py.test =========================== test session starts ============================ platform linux -- Python 3.4.0 -- py-1.4.26 -- pytest-2.6.4 collected 1 items test_sample.py F ================================= FAILURES ================================= _______________________________ test_answer ________________________________ def test_answer(): > assert plusone(3) == 5 E assert 4 == 5 E + where 4 = plusone(3) test_sample.py:5: AssertionError ========================= 1 failed in 0.01 seconds =========================

Slide 12

Slide 12 text

def test_answer(): > assert plusone(3) == 5 E assert 4 == 5 E + where 4 = plusone(3) test_sample.py:5: AssertionError

Slide 13

Slide 13 text

Test discovery • collection starts from the initial command line arguments which may be directories, filenames or test ids. • recurse into directories, unless they match norecursedirs • test_*.py or *_test.py files • Test prefixed test classes (without an __init__ method) • test_ prefixed test functions or methods

Slide 14

Slide 14 text

Context-Sensitive Comparisons

Slide 15

Slide 15 text

assert 'foo 1 bar' == 'foo 2 bar' E - foo 1 bar E ? ^ E + foo 2 bar E ? ^

Slide 16

Slide 16 text

assert 'foo\nspam\nbar' == 'foo\neggs\nbar' E foo E - spam E + eggs E bar

Slide 17

Slide 17 text

a = '1'*100 + 'a' + '2'*100 b = '1'*100 + 'b' + '2'*100 assert a == b E assert '111111111111...2222222222222' == '1111111111111...2222222222222' E Skipping 90 identical leading characters in diff, use -v to show E Skipping 91 identical trailing characters in diff, use -v to show E - 1111111111a222222222 E ? ^ E + 1111111111b222222222 E ? ^

Slide 18

Slide 18 text

assert set(['0', '1', '3', '8']) \ == \ set(['0', '3', '5', '8']) E Extra items in the left set: E '1' E Extra items in the right set: E '5'

Slide 19

Slide 19 text

assert {'a': 0, 'b': 1, 'c': 0} \ == \ {'a': 0, 'b': 2, 'd': 0} E Omitting 1 identical items, use -v to show E Differing items: E {'b': 1} != {'b': 2} E Left contains more items: E {'c': 0} E Right contains more items: E {'d': 0} E Use -v to get the full diff

Slide 20

Slide 20 text

Testing Exceptions

Slide 21

Slide 21 text

# content of test_sysexit.py import pytest def f(): raise SystemExit(1) def test_mytest(): with pytest.raises(SystemExit): f() # (this test will pass)

Slide 22

Slide 22 text

Classic xUnit
 Setup & Teardown

Slide 23

Slide 23 text

# Runs once for this whole module def setup_module(module): print "setup_module" # Runs once for this whole module def teardown_module(module): print "teardown_module"

Slide 24

Slide 24 text

# [setup / teardown module]

Slide 25

Slide 25 text

# [setup / teardown module] class TestDB: # Runs once for all the tests @classmethod def setup_class(cls): print "\tsetup_class" # Runs once for all the tests @classmethod def teardown_class(cls): print "\t teardown_class"

Slide 26

Slide 26 text

# [setup / teardown module] class TestDB: # [setup / teardown class]

Slide 27

Slide 27 text

# [setup / teardown module] class TestDB: # [setup / teardown class] # Runs once for each test in this class def setup_method(self, method): print "\t\t setup_method" # Runs once for each test in this class def teardown_method(self, method): print "\t\t teardown_method"

Slide 28

Slide 28 text

# [setup / teardown module] class TestDB: # [setup / teardown class] # [setup / teardown method]

Slide 29

Slide 29 text

# [setup / teardown module] class TestDB: # [setup / teardown class] # [setup / teardown method] def test_one(self): print "\t\t\t test_one" def test_two(self): print "\t\t\t test_two"

Slide 30

Slide 30 text

setup_module setup_class setup_method test_one teardown_method setup_method test_two teardown_method teardown_class teardown_module

Slide 31

Slide 31 text

Fixtures

Slide 32

Slide 32 text

Fixtures The purpose of test fixtures is to provide a fixed baseline upon which tests can reliably and repeatedly execute. In Pytest, fixtures have explicit names and are activated by declaring their use from test functions, modules, classes or whole projects (via dependency injection).

Slide 33

Slide 33 text

# content of ./test_fixture_example.py import pytest class Person: def greet(): return "Hello there!"

Slide 34

Slide 34 text

# content of ./test_fixture_example.py import pytest class Person: def greet(): return "Hello there!" @pytest.fixture def person(): return Person()

Slide 35

Slide 35 text

# content of ./test_fixture_example.py import pytest class Person: def greet(): return "Hello there!" @pytest.fixture def person(): return Person() def test_greet(person): greeting = person.greet() assert greeting == "Hi there!

Slide 36

Slide 36 text

• pytest sees that test_greet needs a function argument named person • A matching fixture function is discovered by looking for a fixture-marked function named person • person() is called to create an instance • test_greet() is called What's Happening

Slide 37

Slide 37 text

# content of ./test_fixture_example.py import pytest class Person: def greet(): return "Hello there!" @pytest.fixture def person(): return Person() def test_greet(person): greeting = person.greet() assert greeting == "Hi there!

Slide 38

Slide 38 text

> assert greeting == "Hi there!" E assert 'Hello there!' == 'Hi there!' E - Hi there! E + Hello there!

Slide 39

Slide 39 text

• Can call other fixtures • Can be used to parameterize a test function against a collection of test values • Can be shared between test files via conftest.py • @pytest.fixture(autouse=True) acts as setup/teardown without explicit request by your test functions • You can even test your fixtures (they’re just functions) Fixtures are awesome

Slide 40

Slide 40 text

Verifying Calls

Slide 41

Slide 41 text

# content of mock_example.py: class DB: def __init__(self): pass def persist(self, person): pass

Slide 42

Slide 42 text

# content of mock_example.py: class DB: def __init__(self): pass def persist(self, person): pass class Person: def __init__(self, name, db): self.name = name self.db = db def save(self): self.db.persist(self)

Slide 43

Slide 43 text

# content of mock_test.py import pytest from mock import Mock from mock_example import Person, DB

Slide 44

Slide 44 text

# content of mock_test.py import pytest from mock import Mock from mock_example import Person, DB @pytest.fixture def mock_db(): return Mock(spec=DB) # specs to follow . . .

Slide 45

Slide 45 text

# content of mock_test.py (continued) def test_save_persists_to_db(mock_db): gabe = Person("Gabe", mock_db) gabe.save() mock_db.persist.assert_called_with(gabe) # Passes

Slide 46

Slide 46 text

# content of mock_test.py (continued) def test_fail_not_called(mock_db): mock_db.persist.assert_called_with() # E AssertionError: Expected call: persist() # E Not called

Slide 47

Slide 47 text

# content of mock_test.py (continued) def test_fail_called_with_other_arg(mock_db): mock_db.persist(1, 2, 3) mock_db.persist.assert_called_with() # E AssertionError: Expected call: persist() # E Actual call: persist(1, 2, 3)

Slide 48

Slide 48 text

# content of mock_test.py (continued) def test_any_call(mock_db): mock_db.persist(1) mock_db.persist(2) mock_db.persist.assert_any_call(1) # Passes

Slide 49

Slide 49 text

• assert_called_with only tracks the last call • if you call mock('bar') and then try mock.assert_any_call('foo'), it will fail, but it won’t tell you that mock('bar') was called • the docs are your friend Tiny gotchas (read the docs)

Slide 50

Slide 50 text

Stubbing Return Values

Slide 51

Slide 51 text

# content of forecaster.py import random class WeatherService: def barometer(self): # some unpredictable result here (live weather) random.choice(['rising', 'falling'])

Slide 52

Slide 52 text

# content of forecaster.py continued class Forecaster: def __init__(self, weather_service): self.weather_service = weather_service def forecast(self): reading = self.weather_service.barometer() forecasts = dict( rising='Going to rain', falling='Looks clear', ) return forecasts[reading]

Slide 53

Slide 53 text

import pytest from mock import Mock from forecaster import Forecaster, WeatherService @pytest.fixture def mock_ws(): return Mock(spec=WeatherService)

Slide 54

Slide 54 text

import pytest from mock import Mock from forecaster import Forecaster, WeatherService @pytest.fixture def mock_ws(): return Mock(spec=WeatherService) def test_rain_when_barometer_rising(mock_ws): forecaster = Forecaster(mock_ws) mock_ws.barometer.return_value = 'rising' assert forecaster.forecast() == 'Going to rain'

Slide 55

Slide 55 text

import pytest from mock import Mock from forecaster import Forecaster, WeatherService @pytest.fixture def mock_ws(): return Mock(spec=WeatherService) def test_rain_when_barometer_rising(mock_ws): forecaster = Forecaster(mock_ws) mock_ws.barometer.return_value = 'rising' assert forecaster.forecast() == 'Going to rain' def test_clear_when_barometer_falling(mock_ws): forecaster = Forecaster(mock_ws) mock_ws.barometer.return_value = 'falling' assert forecaster.forecast() == 'Looks clear'

Slide 56

Slide 56 text

@pytest.mark.parametrize("reading, expected_forecast", [ ('rising', 'Going to rain'), ('falling', 'Looks clear'), ]) def test_forecast(reading, expected_forecast, mock_ws): forecaster = Forecaster(mock_ws) mock_ws.barometer.return_value = reading assert forecaster.forecast() == expected_forecast Refactored to use a parametrized test

Slide 57

Slide 57 text

Monkeypatching

Slide 58

Slide 58 text

# content of weather_service.py import random class WeatherService: def barometer(self): # some unpredictable result here (live weather) random.choice(['rising', 'falling'])

Slide 59

Slide 59 text

# content of forecaster.py from weather_service import WeatherService class Forecaster: def __init__(self): self.weather_service = WeatherService() # . . . What if I can't inject a mock into my constructor?

Slide 60

Slide 60 text

monkeypatch to the rescue • it's a special predefined fixture • use monkeypatch.setattr() to go in to a module and patch a value • pytest removes the patch when the test function returns

Slide 61

Slide 61 text

def test_rain_when_barometer_rising(monkeypatch, mock_ws): WS = Mock(return_value=mock_ws) monkeypatch.setattr('forecaster.WeatherService', WS) forecaster = Forecaster() mock_ws.barometer.return_value = 'rising' assert forecaster.forecast() == 'Going to rain' Using the monkeypatch fixture

Slide 62

Slide 62 text

Plugins

Slide 63

Slide 63 text

pytest-allure-adaptor-1.5.4 pytest-ansible-1.1 pytest-autochecklog-0.1.2 pytest-bdd-2.6.1 pytest-beakerlib-0.5 pytest-beds-0.0.1 pytest-bench-0.3.0 pytest-benchmark-2.3.0 pytest-blockage-0.1 pytest-bpdb-0.1.4 pytest-browsermob-proxy-0.1 pytest-bugzilla-0.2 pytest-cache-1.0 pytest-cagoule-0.2.0 pytest-capturelog-0.7 pytest-catchlog-1.0 pytest-circleci-0.0.2 pytest-cloud-1.0.15 pytest-codecheckers-0.2 pytest-colordots-0.1 pytest-config-0.0.11 pytest-contextfixture-0.1.1 pytest-couchdbkit-0.5.1 pytest-cov-1.8.1 pytest-cpp-0.3.1 pytest-dbfixtures-0.9.0 pytest-dbus-notification-1.0.1 pytest-describe-0.10 pytest-diffeo-0.1.8.dev3 pytest-django-2.8.0 pytest-django-haystack-0.1.1 pytest-django-lite-0.1.1 pytest-django-sqlcount-0.1.0 pytest-echo-1.3 pytest-env-0.5.1 pytest-eradicate-0.0.2 pytest-figleaf-1.0 pytest-fixture-tools-1.0.0 pytest-flakes-0.2 pytest-flask-0.7.2 pytest-greendots-0.3 pytest-growl-0.2 pytest-httpbin-0.0.6 pytest-httpretty-0.2.0 pytest-incremental-0.3.0 pytest-instafail-0.3.0 pytest-ipdb-0.1-prerelease2 pytest-ipynb-0.1.1 pytest-jira-0.01 pytest-knows-0.1.5 pytest-konira-0.2 pytest-localserver-0.3.4 pytest-marker-bugzilla-0.06 pytest-markfiltration-0.8 pytest-marks-0.4 pytest-mock-0.4.0 pytest-monkeyplus-1.1.0 pytest-mozwebqa-1.5 pytest-multihost-0.6 pytest-oerp-0.2.0 pytest-oot-0.3.1 pytest-optional-0.0.2 pytest-ordering-0.3 pytest-osxnotify-0.1.5 pytest-paste-config-0.1 pytest-pep257-0.0.1 pytest-pep8-1.0.6 pytest-pipeline-0.1.0 pytest-poo-0.2 pytest-poo-fail-1.1 pytest-pycharm-0.3.0 pytest-pydev-0.1 pytest-pyq-1.1 pytest-pythonpath-0.6 pytest-qt-1.2.3 pytest-quickcheck-0.8.1 pytest-rage-0.1 pytest-raisesregexp-2.0 pytest-random-0.02 pytest-readme-1.0.0 pytest-regtest-0.5.1 pytest-remove-stale-bytecode-1.0 pytest-rerunfailures-0.05 pytest-runfailed-0.3 pytest-runner-2.3 pytest-services-1.0.4 pytest-sftpserver-1.1.0 pytest-smartcov-0.1 pytest-sourceorder-0.4 pytest-spec-0.2.24 pytest-splinter-1.2.10 pytest-stepwise-0.2 pytest-sugar-0.3.6 pytest-timeout-0.4 pytest-tornado-0.4.2 pytest-translations-0.2.0 pytest-twisted-1.5 pytest-unmarked-1.1 pytest-watch-2.0.0 pytest-xdist-1.11 pytest-xprocess-0.8 pytest-yamlwsgi-0.6 pytest-zap-0.2

Slide 64

Slide 64 text

Invoke the python debugger for each failure
 pytest --pdb
 
 Debug first failure, then stop running tests
 pytest -x --pdb
 
 Prefer ipdb? Use mverteuil/pytest-ipdb
 pip install \ 
 git+git://github.com/mverteuil/pytest-ipdb.git 
 pytest --ipdb


Slide 65

Slide 65 text

Time for one more tip?

Slide 66

Slide 66 text

import pytest class Developer: def __init__(self, favorite): self.loves=favorite def brag(self): return self.loves + " is the best!"

Slide 67

Slide 67 text

# class Developer . . .

Slide 68

Slide 68 text

# class Developer . . . @pytest.fixture def language(): return 'python' @pytest.fixture def developer(language): return Developer(language)

Slide 69

Slide 69 text

# class Developer . . . # fixtures . . .

Slide 70

Slide 70 text

# class Developer . . . # fixtures . . . def test_brag(developer): assert developer.brag() == "python is the best!"

Slide 71

Slide 71 text

# class Developer . . . # fixtures . . . # test_brag . . . What if we want to make this developer instance love ruby instead?

Slide 72

Slide 72 text

# class Developer . . . # fixtures . . . # test_brag . . . You can parametrize dependent fixtures!

Slide 73

Slide 73 text

# class Developer . . . # fixtures . . . # test_brag . . . @pytest.mark.parametrize('language', ['ruby']) def test_brag_works_for_ruby(developer): assert developer.brag() == "ruby is the best!"

Slide 74

Slide 74 text

# class Developer . . . @pytest.fixture def language(): return 'python' @pytest.fixture def developer(language): return Developer(language)

Slide 75

Slide 75 text

That's all for today.

Slide 76

Slide 76 text

Where to read more • pytest.org • www.voidspace.org.uk/python/mock

Slide 77

Slide 77 text

Questions?

Slide 78

Slide 78 text

Thank you! @gabehollombe [email protected]