testing with pytest
Introduction to testing with pytest
what Is the purpose?
Verify code logic.
what Is the purpose?
Think the design through.
what Is the purpose?
Documentation.
what Is the purpose?
Protection against mistakes.
testing concepts
What needs to be tested?
Everything.
If something can't be tested that's the exception, not the rule.
testing concepts
Unit testing are low level tests.
They test particular methods/classes in isolation.
Third-party systems are mocked through libraries or patched by us.
testing concepts
Integration testing verifies different components working together.
testing concepts
No internet access is required.
testing concepts
Tests do not use source code for arranging scenarios.
testing concepts
Tests do not contain logic.
testing concepts
Tests follow the same clean-code rules as the code.
Meaningful names, single responsibility principle, don't repeat yourself, etc.
testing concepts
Code coverage is meaningless without proper tests.
def is_active(user: Optional[User] = None):
return user.active
def test_user_is_active():
assert is_active(User(active=true))
testing concepts
Each line of code has a meaning.
If a line doesn't have a meaning must be removed.
what's pytest?
Testing framework for python.
what does it provide?
It runs tests.
what does it provide?
It organises tests.
what does it provide?
It provides context.
# Receives <FixtureRequest for <Function test_context>>
def test_context(request: FixtureRequest):
print(request)
assert False
# <SubRequest 'this_is_a_fixture' for <Function test_context>>
@pytest.fixture
def this_is_a_fixture(request: SubRequest):
print(request)
setup/teardown
Achieved through "fixtures".
setup/teardown
A "fixture" must do one thing, and only one thing.
setup/teardown
Reusable.
setup/teardown
Built-in.
setup/teardown
Flexible scopes.
@pytest.fixture(scope=function|class|module|package|session)
def this_is_a_fixture():
pass
- function: destroyed at the end of the test.
- class: destroyed during the teardown of the last test in the class.
- module: destroyed during the teardown of the last test in the module.
- package: destroyed during the teardown of the last test in the package.
- session: destroyed at the end of the test session.
setup/teardown
Implicit dependencies.
@pytest.fixture
def this_is_a_fixture():
pass
@pytest.fixture
def this_is_another_fixture(this_is_a_fixture):
pass
setup/teardown
Allows parametrisation.
@pytest.fixture(params=["a", "b"])
def this_is_a_fixture(request):
return request.param
def test_something(this_is_a_fixture):
print(this_is_a_fixture)
assert True
Using the fixture will run the test once for each of the params.
setup/teardown
Factories as fixtures.
@pytest.fixture
def run_something(capsys):
def _runner(data: str):
print(data)
return _runner
def test_run_something_bla(run_something, capsys):
run_something("bla")
assert capsys.readouterr().out == "bla"
def test_run_something_foo(run_something, capsys):
run_something("foo")
assert capsys.readouterr().out == "foo"
setup/teardown
Teardown using generators.
@pytest.fixture
def this_is_a_fixture(capsys):
print("this will happen before the test")
yield
print("this will happen after the test")
def test_something(this_is_a_fixture):
assert True
markers
Set metadata for our tests.
markers
They are processed before fixtures.
markers
They can not be applied to fixtures.
markers
They can pass metadata to fixtures.
@pytest.fixture
def this_is_a_fixture(request):
mark = request.node.get_closest_marker("gorka")
print(f"mark args={mark.args} kwargs={mark.kwargs}")
@pytest.mark.gorka("arg", kwarg="kwarg")
def test_something(this_is_a_fixture):
assert True
markers
Use "usefixtures" if you don't need the fixture return value.
@pytest.fixture
def this_does_something(request):
pass
def test_something(this_does_something):
assert True
@pytest.fixture
def this_does_something(request):
pass
@pytest.mark.usefixtures("this_does_something")
def test_something():
assert True
parametrizing
Run multiple variations of same test.
parametrizing
Tests as specific as possible.
def test_run_has_arguments(run):
assert run.args == {
"keyA": "valueA",
"keyB": "valueB",
}
@pytest.mark.parametrize("key, value", [
("keyA", "valueA"),
("keyB", "valueB"),
])
def test_run_has_argument(
run, key, value
):
assert run.args[key] == value
parametrizing
Permutations of the same test.
@pytest.mark.parametrize("name", [
"Chris", "Laura"
])
@pytest.mark.parametrize("surname", [
"Hemsworth", "Pausini"
])
def test_full_name_composition(name, surname):
assert full_name(name, surname) == f"{name} {surname}"
This will run the test 4 times, for each combination of parameters.
parametrizing
Override test meaning.
@pytest.mark.parametrize("user_id, expected", [
("111", "active"),
("222", "inactive")
("333", "inactive"),
], ids=[
"user active",
"user inactive",
"user deleted",
])
def test_user_status(user_id, expected):
assert User(user_id).status == expected
parametrizing
Override test meaning.
@pytest.mark.parametrize("user_id, expected", [
pytest.param("111", "active", id="user active"),
pytest.param("222", "inactive", id="user inactive")
pytest.param("333", "inactive", id="user deleted"),
])
def test_user_status(user_id, expected):
assert User(user_id).status == expected
parametrizing
Indirect parametrising of fixtures.
@pytest.fixture
def user(request):
return User(request.param)
@pytest.mark.parametrize("user, expected", [
pytest.param("111", "active", id="user active"),
pytest.param("222", "inactive", id="user inactive")
pytest.param("333", "inactive", id="user deleted"),
], indirect=["user"])
def test_user_status(user, expected):
assert user.status == expected
patching
Replace parts of your system under test with mock objects.
patching
Assert how those mocks have been used.
patching
Patching at test level.
@mock.patch.object(os, "getenv")
def test_host_is_retrieved_from_environment(getenv):
run_something()
getenv.assert_called_with("SERVICE_HOST")
patching
Patching at test level without mock assertion.
@mock.patch.object(os, "getenv", new=MagicMock(return_value="foo"))
def test_host_is_retrieved_from_environment():
assert run_something()
patching
Patching at fixture level.
@pytest.fixture
def getenv():
with mock.patch.object(os, "getenv") as mocked:
yield mocked
def test_host_is_retrieved_from_environment(getenv):
run_something()
getenv.assert_called_with("SERVICE_HOST")
patching
Patching from dependencies.
@pytest.fixture(params=["dev", "staging"])
def environment(request):
return request.param
@pytest.fixture
def get_environment(environment):
with mock.patch("get_environment", return_value=environment) as mocked:
yield mocked
def test_get_environment_is_used(get_environment):
run_something()
get_environment.assert_called()
def test_path_points_to_environment(environment):
assert run_something().path == f"path/{environment}"
patching
Patching from parameters.
@pytest.fixture(params=["dev", "staging"])
def environment(request):
return request.param
@pytest.fixture
def get_environment(environment):
with mock.patch("get_environment", return_value=environment) as mocked:
yield mocked
@pytest.mark.parametrize("environment", ["production"], indirect=True)
def test_path_does_not_have_environment_in_production():
assert run_something().path == "path"
patching
Patching factory as fixture.
@pytest.fixture(params=["dev", "staging"])
def environment(request):
return request.param
@pytest.fixture
def get_environment(environment):
with mock.patch("get_environment", return_value=environment) as mocked:
yield mocked
@pytest.fixture
def launcher(get_environment):
def _runner(data: str):
return Launcher(data)
yield _runner
@pytest.fixture
def test_path_contains_environment(launcher, environment):
assert launcher("foo").path == f"{environment}/foo"
sources
courses
Testing with Pytest
By Gorka Guridi
Testing with Pytest
Introduction to testing with pytest
- 132