Our team uses the Pytest framework to write system tests for our API product. I’ve been really pleased with how simple, flexible, and unfussy the tool is to work with. There’s plenty of room for creative usage of just its core features to write many different types of tests. In particular, our team has relied heavily on Pytest fixtures to handle a variety of tasks for setting up system tests.
Here, I’ll highlight how we apply some simple options for configuring fixture usage. This will allow you to do things like create API and DB clients, wait for external services to start up, and set up/tear down test scenarios.
Fixture Scoping
You can create fixtures with a variety of different scopes. For system tests, session
scoped fixtures are appropriate for one-time setup of the test environment. This includes reading secrets from the environment and initializing API or DB clients for interacting with the system under test.
Also, note how fixtures can also be requested by other fixtures. This allows us to isolate loading environment configuration into just one spot, for reuse between tests and other fixtures alike.
import os
from api_client import ApiClient
from database_client import DatabaseClient
@pytest.fixture(scope="session")
def target_config(request):
return {
"base_url": os.environ.get("BASE_URL"),
"health_endpoint": os.environ.get("HEALTH_ENDPOINT")
"database_url": os.environ.get("DATABASE_URL"),
}
@pytest.fixture(scope="session")
def api_client(target_config):
return ApiClient(target_config)
@pytest.fixture(scope="session")
def database_client(target_config):
return DatabaseClient(target_config)
Autouse Fixtures
You can tag fixtures as “autouse” (also as part of the fixture decorator). This configures the fixture to execute without you explicitly having to request it, allowing us to use it like a before hook.
So, for example, combined with setting a session scope, we can use an autouse fixture to perform a simple health check that waits for external services to start up before beginning test execution.
import time
from test_utils import check_external_service_health
@pytest.fixture(scope="session", autouse=True)
def wait_for_services(target_config):
max_retries = 10
retries = 0
ready = False
while retries < max_retries and not ready:
if not check_external_service_health(target_config):
time.sleep(10)
retries += 1
else:
ready = True
In a similar fashion, we can use a `function` scoped fixture, which will execute before each test function.
from test_utils import reset_external_services
@pytest.fixture(scope="function", autouse=True)
def reset_test_environment(target_config):
reset_external_services()
Setting these fixtures to autouse will ensure that the test environment is always up and running. It will also reset with a blank slate, even if a developer forgets to include them in the requested fixtures.
Fixture Teardown
Our Pytest integration test suite supports either running tests against mocked backend services and a test DB or against a live environment. (This is great for things like release validation and scale testing.) In the latter case, simply throwing away any and all created resources is not an option. For that reason, Pytest's fixture teardown is a nice way to help ensure that any limited resources created also get cleaned up.
Our team uses DIY Factories to simulate different scenarios in our system tests. The linked post will walk you through an example of using fixtures to generate, `yield`, and clean up test data.
Pytest Fixtures for System Testing
This is only scratching the surface of the ways you can use Pytest to create and customize your test suite. I hope that these practical examples provided a good start with understanding how to leverage Pytest fixtures for system testing!