The py.test framework has many great features, but this talk focuses on the fixtures.
Test fixtures are “things that the code under test relies on, but that you don’t want to test itself”. The traditional way of defining fixtures is to create setup and teardown functions at the level of a test, a class, a module. The problem with this approach is that it is not easy to compose, because everything that needs to be set up is mixed together.
py.test fixtures work differently. When there is a function that takes arguments, py.test will look for a fixture function (annotated with @pytest.fixture) and run that as a setup function. For teardown, the setup function calls request.addfinalizer to register a finaliser. It’s convenient to do it this way because you can use a closure, so you don’t need to store things in global variables or anything like that. Finalisers are executed in reverse order of how they were created.
Fixtures can be reused in several tests. If you put them in conftest.py in your test directory, py.test will pick them up and define them for all tests in that directory.
Running ‘py.test -fixtures’ gives a list of fixtures that are known. There are a bunch of built-in fixtures, and plugins that define fixtures (plugins are loaded by importing them in conftest.py). For instance monkeypatch is a fixture that patches existing functions and changes what they do – best to combine with the mock module, that replaces a function with a mock that just returns some predefined values. tmpdir is a fixture that helps creating temporary directories and files inside them. The temporary directory is not removed immediately, only after a number of directories have been created, so if a test goes wrong you can see what is left there.
Since creating fixtures can be expensive (e.g. filling up a database), py.test can be instructed to keep it alive for some scope lifetime. Default is test scope, but it can be e.g. module scope or session scope.
Fixtures can request other fixtures, just by adding arguments to the fixture function signature. These are looked up in the same way as a fixture on a test. The scopes of the fixtures must match however.
Fixtures can be parameterised, which basically creates multiple copies of the fixture. Whenever there is a test that requests that fixture, it will be run as many times as there are parameters. The parameter values can be requested from within the fixture.
Fixtures can call pytest.skip to skip the test in case the setup failed – e.g. when the database server is not available. But of course you don’t want that in you CI, so then you can add a command-line option with pytest_addoption to decide if it should fail or skip.
Autouse fixtures are run for each test function even if they don’t request the fixture explicitly. This can be combined with pytest markers (@pytest.mark.XXXX) to skip tests in certain situations (e.g. if test is marked osx_only, check the OS and if it’s not OS-X, skip it).