is a python test framework
is a python test framework
makes it easy to write small tests
is a python test framework
makes it easy to write small tests
scales to support complex testing
is a python test framework
makes it easy to write small tests
scales to support complex testing
helps you write better code
Tests show that your code works.
Testing improves confidence that your code changes are non-breaking.
Tested code is less error-prone.
Tests serve as living documentation.
Testable code is more maintainable and understandable code.
Introducing swapy,
the exciting new python library which wraps swapi!
But what is swapi?
... the Star Wars API!
swapi is an excellent, free, and completely open API which supports both JSON and Wookiee data formats
I built a toy API wrapper (swapy) around swapi to provide some actual working code that we can test using pytest
Install pytest (if you don't have it installed already) via your favorite env flavor:
pip install pytest
conda install pytest
Clone the repo from here:
swapy/
tests/
The toy codebase lives in this folder.
example_test_suite/
test_tutorials/
This folder contains an example of a working test suite for swapy.
This folder contains the code for the examples in these slides.
swapy architecture and design
The wrapper code was written to be simple, functional, and,
most importantly, testable.
pytest integrates with popular editors such as PyCharm and Sublime Text, but we are going to kick it old school with the command line.
It all starts with the pytest command.
Note that all commands will be relative to your pytest-tutorial directory, i.e wherever you cloned the repo, so:
YMMV on the pytest output displayed in these slides versus exactly what you see on your machine (given differences in environments), but it should be reasonably similar.
cd wherever/you/cloned/pytest-tutorial
pytest tests/test_tutorials/test_01_run_pytest/
==================================================== test session starts =====================================================
platform darwin -- Python 3.7.3, pytest-5.1.2, py-1.8.0, pluggy-0.13.0
rootdir: /Users/rachel_house/dev/pytest-tutorial
plugins: arraydiff-0.3, doctestplus-0.4.0, openfiles-0.4.0, pylint-0.14.1, remotedata-0.3.1, cov-2.7.1, dash-1.3.0
collected 1 item
tests/test_tutorials/test_01_run_pytest/test_01.py F [100%]
========================================================== FAILURES ==========================================================
__________________________________________________ test_create_swapy_object __________________________________________________
def test_create_swapy_object():
> swapy = Swapy(use_cache=False, my_crazy_arg='trekkies4eva')
E TypeError: __init__() got an unexpected keyword argument 'my_crazy_arg'
tests/test_tutorials/test_01_run_pytest/test_01.py:8: TypeError
===================================================== 1 failed in 0.18s ======================================================
Run this command:
Now take a look at the results:
Let's unpack what just happened.
1. We wrote a .py file containing a test.
We used assert to verify test expectations, namely,
that the newly init'd Swapy object would successfully init and have the correct default _wookie setting.
import pytest
from swapy import Swapy
def test_create_swapy_object():
swapy = Swapy(use_cache=False, my_crazy_arg='trekkies4eva')
assert swapy._wookiee == False, 'default wookiee setting should be false'
2. We ran pytest on the command line.
Note that we pointed pytest to the directory, not the actual file.
pytest autodiscovered the appropriate test(s) to run.
pytest tests/test_tutorials/test_01_run_pytest/
pytest determines a rootdir for each test run which depends on the command line arguments (specified test files, paths) and on the existence of ini-files.
Also relevant and useful to know...
==================================================== test session starts =====================================================
platform darwin -- Python 3.7.3, pytest-5.1.2, py-1.8.0, pluggy-0.13.0
rootdir: /Users/rachel_house/dev/pytest-tutorial
plugins: arraydiff-0.3, doctestplus-0.4.0, openfiles-0.4.0, pylint-0.14.1, remotedata-0.3.1, cov-2.7.1, dash-1.3.0
collected 1 item
tests/test_tutorials/test_01_run_pytest/test_01.py F [100%]
========================================================== FAILURES ==========================================================
__________________________________________________ test_create_swapy_object __________________________________________________
def test_create_swapy_object():
> swapy = Swapy(use_cache=False, my_crazy_arg='trekkies4eva')
E TypeError: __init__() got an unexpected keyword argument 'my_crazy_arg'
tests/test_tutorials/test_01_run_pytest/test_01.py:8: TypeError
===================================================== 1 failed in 0.18s ======================================================
The determined rootdir and ini-file are printed as part of the pytest header during startup.
(no ini-file specified in this example)
3. Something clearly went wrong with the test.
==================================================== test session starts =====================================================
platform darwin -- Python 3.7.3, pytest-5.1.2, py-1.8.0, pluggy-0.13.0
rootdir: /Users/rachel_house/dev/pytest-tutorial
plugins: arraydiff-0.3, doctestplus-0.4.0, openfiles-0.4.0, pylint-0.14.1, remotedata-0.3.1, cov-2.7.1, dash-1.3.0
collected 1 item
tests/test_tutorials/test_01_run_pytest/test_01.py F [100%]
========================================================== FAILURES ==========================================================
__________________________________________________ test_create_swapy_object __________________________________________________
def test_create_swapy_object():
> swapy = Swapy(use_cache=False, my_crazy_arg='trekkies4eva')
E TypeError: __init__() got an unexpected keyword argument 'my_crazy_arg'
tests/test_tutorials/test_01_run_pytest/test_01.py:8: TypeError
===================================================== 1 failed in 0.18s ======================================================
pytest kindly displays what tests it ran, and, for any failures, where the error occured in in the test.
Here, we can see that our mistake was trying to start a holy war between Star Wars and Star Trek.
Let's amend this error and rerun pytest.
pytest tests/test_tutorials/test_02_run_pytest_no_failure/
import pytest
from swapy import Swapy
def test_create_swapy_object():
swapy = Swapy(use_cache=False)
assert swapy._wookiee == False, 'default wookiee setting should be false'
======================================================= test session starts ========================================================
platform darwin -- Python 3.7.3, pytest-5.1.2, py-1.8.0, pluggy-0.13.0
rootdir: /Users/rachel_house/dev/pytest-tutorial/tests
plugins: arraydiff-0.3, doctestplus-0.4.0, openfiles-0.4.0, pylint-0.14.1, remotedata-0.3.1, cov-2.7.1, dash-1.3.0
collected 1 item
test_tutorials/test_02_run_pytest_no_failure/test_02.py . [100%]
======================================================== 1 passed in 0.02s =========================================================
Success!
We can see from the pytest output that our corrected test passed.
Congratulations!
You have now used pytest to run a simple failing and passing test.
Just one more thing before we move onto pytest fixtures...
Previously, we saw one of our tests fail because we supplied an incorrect argument when initializing the Swapy() object.
Testing that a known, expected failure occurs is a useful thing to do, and pytest provides an easy mechanism to do so: pytest.raises
import pytest
from swapy import Swapy
def test_create_swapy_object_fails_with_bad_args():
with pytest.raises(TypeError):
swapy = Swapy(use_cache=False, my_crazy_arg='trekkies4eva')
pytest tests/test_tutorials/test_03_pytest_raises/
Test fixtures provide a fixed baseline upon which tests can reliably and repeatedly execute.
Fixtures allow your test functions to easily receive and work against specific pre-initialized application objects.
It’s dependency injection: fixture functions take the role of the injector and test functions are the consumers of fixture objects.
Bottom line: test fixtures are incredibly useful, and pytest makes them super easy to create and use.
Test functions can receive pytest fixture objects by naming them as an input arguments.
pytest fixture functions are registered by marking them with the @pytest. fixture decorator
import pytest
from swapy import SwapyBase
@pytest.fixture()
def swapi_base_url():
return 'https://swapi.co/api'
def test_assemble_swapi_url(swapi_base_url):
'''Test that swapi api request urls are assembled correctly'''
swapy_base = SwapyBase()
# no args to _assemble_swapi_url yields swapi base url
assert swapy_base._assemble_swapi_url() == swapi_base_url
# test return of swapi schema url
resource = 'planets'
expected_url = '{}/planets/schema'.format(swapi_base_url)
assert swapy_base._assemble_swapi_url(resource, schema=True) == expected_url
import pytest
from swapy import SwapyBase
@pytest.fixture()
def swapi_base_url():
return 'https://swapi.co/api'
def test_assemble_swapi_url(swapi_base_url):
'''Test that swapi api request urls are assembled correctly'''
swapy_base = SwapyBase()
# no args to _assemble_swapi_url yields swapi base url
assert swapy_base._assemble_swapi_url() == swapi_base_url
# test return of swapi schema url
resource = 'planets'
expected_url = '{}/planets/schema'.format(swapi_base_url)
assert swapy_base._assemble_swapi_url(resource, schema=True) == expected_url
pytest tests/test_tutorials/test_04_simple_fixture/
But wait, there's more!
Cool, right?
conftest.py allows you to share fixture functions across multiple test files
Stick it in your root directory, define those fixtures, and get ready for some sweet fixture reuse across your test suite.
pytest --fixtures test_directory_or_file/
A handy hint:
will show you all your currently defined and available fixtures
Code often contain dependencies.
Dependencies can include such things as:
At test time, we may not be able to (nor want to) access these dependencies:
Mocking allows you to create values, functions, and objects which simulate the behavior of your dependencies.
Mocking is a powerful testing technique.
Possibly more powerful than The Force.
The pytest mechanism for mocking is called monkeypatching.
monkeypatching functions
(slides to come, we'll just dive into the code now)
extremely useful pytest fixtures
tmpdir
(under construction)
datadir
(also under construction)
The debugger is a place where you can step through the guts of your code to see what is happening.
It is also very useful for diagnosing failing tests.
pytest --pdb
pytest --trace
Drop into debugger on every failure (or KeyboardInterrupt)
Drop into debugger at the start of every test
p | print (value of expression) |
pp | pretty print |
l | list (print) lines of code around current position |
n | continue to next line |
s | step into |
b | set a breakpoint |
c | continue execution until next breakpoint or end |
q | quit |
pytest marks
the pytest.mark decorator lets you set metadata on your test functions
You can then run tests based on your pytest marks.
To get a list of the n slowest test durations run:
pytest --durations=n
Also a useful little trick:
The Pytest Website:
https://docs.pytest.org/en/2.8.7/index.html
pytest PDF Documentation:
https://media.readthedocs.org/pdf/pytest/latest/pytest.pdf
The Star Wars API (SWAPI)
https://swapi.co/