This post is a quick tutorial to pytest, an awesome testing framework for Python. The main goal of the tutorial is to show features of pytest that are most useful in day to day life. Who can benefit from this tutorial?
- Experienced developer, who uses Python occasionally and needs to rewind the syntax and features of pytest.
- A developer who never used pytest and wants to get started in less than 5 minutes.
- A developer who is looking to fill in gaps and learn few tricks using pytest.
All code examples that are shown below can be found here. More exhaustive details on pytest can be found in pytest documentation.
How to get started?
It’s extremely simple, just install pytest from PyPI.
pip install pytest
or
python -m pip install pytest
How to run tests?
Most likely you will have pytest in your path, so to run it’s as simple as
pytest
In this case you may need to add conftest.py
to the root of you folder so pytest will extend sys.path()
and will be able to discover all you modules withing the root directory.
You can also run pytest as python module. There is no need for any tricks or conftest.py
files then. Just simply run
python -m pytest
You can point pytest to run certain folder with tests
pytest tests/unit
Or point it to run specific test module (a file).
pytest tests/unit/test_my_cool_funtionality.py
Or even a specific test function from a specific module.
pytest tests/unit/test_my_cool_funtionality.py::test_division
Of course all of this makes it super friendly to CI /CD tools.
Awesome test results output
Output is another strong feature of pytest. When tests pass output look green.
However, when test(s) fails for every failed test pytest shows the failing assert, exact code snippet, all captured standard output and exception details. It makes it super easy to identify and fix a problem.
Creating a test
The simplest way to create a test is to
- Create a file
test_*.py
- Add a function
def test_*()
- That is all 😊
Example:
import src.division as dv def test_divide(): assert 4 == dv.divide(8, 2)
To run use the command below.
python -m pytest tests/unit/test_simple_division.py
Parameterized tests
Parameterization is a super important feature that allows to avoid lots of copy paste. Many other testing frameworks support parameterized tests, e.g. nUnit and xUnit in .NET. Luckily pytest has great support for parameterized tests too.
It is simple to create a parameterized test, here is how
import pytest import src.division as dv @pytest.mark.parametrize( "dividend, divisor, expected_result", [ # (dividend, divisor, expected_result) (6, 3, 2), # <- Test case 1 (-6, 3, -2), # <- Test case 2 (6, -3, -2), # <- Test case 3 (-6, -3, 2), # <- Test case 4 (0, 3, 0), # <- Test case 5 ]) def test_divide(dividend, divisor, expected_result): assert expected_result == dv.divide(dividend, divisor)
To run the test, use command below.
python -m pytest tests/unit/test_simple_division_parameterized.py
Test for Exceptions
Very often we need to test that our code throws appropriate exception when some invariant is not met. We can definitely implement that with try-catch, but there is much more elegant and simpler pytest solution. You can take advantage of pytest.raises(exception_here)
in conjunction with context manager.
Example:
import pytest import src.division as dv def test_divide_by_zero(): with pytest.raises(ZeroDivisionError): dv.divide(2, 0)
To run the test, use command below.
python -m pytest tests/unit/test_simple_division_exceptions.py
Fixtures
Fixtures are jewels of pytest. I haven’t seen similar implementations in any other framework. Of course, there is support for tests setup and tear down methods in other frameworks, but nothing like pytest fixtures.
What is pytest fixture
Pytest Fixture is a function, it contains code that can be run before and after the test. Fixture may or may not return a value. Pytest fixtures have different scope, e.g. session, module, class, function or recently introduced dynamic. Fixture can be auto-applied to all tests based on scope. Fixture may read test function context. Fixture can be shared using conftest.py
file, so there is no need to import fixtures as pytest will auto-discover them. There are so much more things fixtures can do, but we want to be concise, so let’s focus on the most useful and often used features.
If you’d like to know more details, here is pytest fixtures documentation.
Simple Fixture
To create a fixture we just need to write a function and apply @pytest.fixture
decorator to it.
import pytest @pytest.fixture def simple_fixture(): print("SETUP : simple_fixture") yield print("TEARDOWN : simple_fixture")
To use fixture with a test function we have two options.
- Use decorator
@pytest.mark.usefixtures(fixture1, fixture2, …)
. - Pass fixture as test function argument. This works with parameterized tests too!
import pytest @pytest.mark.usefixtures("simple_fixture") def test_fixture_with_usefixtures_attribute(): print(f"RUNNING : test {__name__}") # Make test fail to see the output assert False def test_fixture_as_function_parameter(simple_fixture): print(f"RUNNING : test {__name__}") # Make test fail to see the output assert True
Fixture work well with parameterized tests too.
import pytest @pytest.mark.parametrize( "param_1, param_2", [ (1, 2), (3, 4) ]) def test_fixture_as_function_parameter_with_parameterized_tests(simple_fixture, param_1, param_2): print(f"RUNNING : test {__name__} with parameters {param_1}, {param_2}") # Make test fail to see the output assert True
To run the test, use command below.
python -m pytest -s -v tests/unit/test_fixture_simple.py
Fixture returning value
Fixtures returning value are very handy when we need to obtain a resource before test runs and release it when test ends. A fixture can create and initialize a resource on setup and release it on tear down A resource can be a database connection, a temporary directory or file that needs to be created and then deleted.
Here is an example of DbConnection
being opened and closed by a fixture.
import pytest class DbConnection(): def __init__(self): self.is_open = True def open(self): self.is_open = True print("DB connection is open") def execute_query(self, query): if not self.is_open: raise Exception("Connection is not open") print(f"Executing DB query: {query}") def close(self): self.is_open = False print("DB connection is closed") @pytest.fixture def db_connection(): db_con = DbConnection() db_con.open() yield db_con db_con.close() def test_fixture_returns_value(db_connection): db_connection.execute_query("SELECT * FROM dbo.Cats") # Make test fail to see the output assert True
To run the test, use command below.
python -m pytest -s -v tests/unit/test_fixture_return_value.py
Fixtures Scope
Fixture scopes determine when and how many times a fixture will run. If we apply multiple fixtures, the scope will determine the order at which fixtures are run. Higher scope of a fixture the earlier it will run setup code earlier and the later it will run tear down code.
- function scope – runs every time a test function is run (default).
- class scope – runs once per test class, regardless how may test methods the class has.
- module scope – runs once per test module.
- session scope – runs once per test session. Test session starts when you run pytest and ends when pytest exists.
- dynamic scope.
Here is an example of session, module and function scoped fixtures applied to a single test function. The example also illustrates the order fixtures code run.
import pytest @pytest.fixture(scope="session") def session_fixture(): print("session_fixture setup") yield print("session_fixture teardown") @pytest.fixture(scope="module") def module_fixture(): print("module_fixture setup") yield print("module_fixture teardown") @pytest.fixture def function_fixture(): print("function_fixture setup") yield print("function_fixture teardown") def test_fixture_scope(module_fixture, session_fixture, function_fixture): print(f"RUNNING : test {__name__}") assert True
To run the test, use command below.
python -m pytest -v -s tests/unit/test_fixture_scope.py
Auto Applied Fixture
Fixtures can automatically run without being explicitly applied or passed as an argument to a test function. We just need to pass autouse=True
to a fixture decorator. The order at which auto fixture code gets executed is determined by the fixture scope. Auto fixtures are executed before non-auto fixtures withing the same scope.
Here is a example from the above, except that all fixtures are autouse
and we do not explicitly pass them as test function parameters.
import pytest @pytest.fixture(scope="session", autouse=True) def session_fixture(): print("session_fixture setup") yield print("session_fixture teardown") @pytest.fixture(scope="module", autouse=True) def module_fixture(): print("module_fixture setup") yield print("module_fixture teardown") @pytest.fixture(autouse=True) def function_fixture(): print("function_fixture setup") yield print("function_fixture teardown") def test_fixture_scope(): print(f"RUNNING : test {__name__}") assert True
To run the test, use command below.
python -m pytest -v -s tests/unit/test_fixture_autouse.py
conftest.py and auto-fixtures
It is really worth to mention that if auto-fixtures are placed in a conftest.py
file they will auto-apply to all test functions and classes within the same and below levels of directory structure. That is very powerful way to ensure that fixtures are applied without the need of importing.
Reading test context in a fixture
There are a lot of built in fixtures in pytest, one of which is request
, it helps to capture current test function context and make it available to a fixture.
Code below shows an example of how we can read module variable from a fixture.
import pytest MODULE_VARIABLE = "MODULE_VARIABLE value" @pytest.fixture def awesome_fixture(request): print("function_fixture setup, running from") print(f"Module: '{request.module.__name__}''") print(f"Function: '{request.function.__name__}''") print(f"Module variable: {request.module.MODULE_VARIABLE}") print("SETUP") yield print("TEARDOWN") def test_fixture_context(awesome_fixture): print(f"RUNNING : {__name__}") assert True
To run the test, use command below.
python -m pytest -v -s tests/unit/test_fixture_capture_context.py
Summary
I hope you find this short tutorial useful. We covered the most useful pytest features that will make you productive every day. If you think I missed something or you have a pytest productivity trick, please share in the comments below!
2 thoughts on “pytest in 5 minutes”