Python tutorials > Testing > pytest > What are pytest fixtures?
What are pytest fixtures?
Pytest fixtures are a powerful feature that allows you to define reusable setup and teardown logic for your tests. They provide a way to manage the dependencies and state required by your tests, making them more organized, readable, and maintainable. Think of them as resources or preconditions that your tests need to run effectively.
Basic Fixture Definition
This code defines a fixture named `database_connection`. The `@pytest.fixture` decorator marks it as a fixture. The fixture's purpose is to simulate setting up a database connection before a test runs and closing it afterwards. The `yield` statement is crucial. Everything before `yield` is setup code that runs before the test. The value yielded (in this case, the `connection` object) is what's injected into the test function as an argument. Everything after `yield` is teardown code that runs after the test, even if the test fails. The `test_using_database` function receives the `database_connection` fixture as an argument, allowing it to use the database connection within the test. The output shows connection, running test and closing connection.
import pytest
@pytest.fixture
def database_connection():
# Setup: Connect to the database
connection = connect_to_database()
print("\nConnected to database!")
yield connection # Provide the fixture value to the test
# Teardown: Close the connection
connection.close()
print("\nClosed database connection!")
def connect_to_database():
#Simulate the database connection for example purposes
print("Connecting to database...")
return type('DBConnection', (object,), { 'is_connected': True, 'close': lambda self: print('Closing connection...') })()
def test_using_database(database_connection):
# Use the database connection provided by the fixture
assert database_connection.is_connected == True
print("\nTesting with database connection...")
Concepts Behind the Snippet
The core concept is dependency injection. Pytest automatically manages the creation and cleanup of the resources your tests need. This avoids repetitive setup/teardown code in each test function, promoting DRY (Don't Repeat Yourself) principles. Fixtures significantly improve test readability and maintainability. The `yield` statement is key to handling resources that need to be cleaned up after use.
Real-Life Use Case: API Testing
In API testing, you often need an authenticated API client. This fixture sets up a `requests.Session` object with the base URL and authentication headers. The test function then uses this `api_client` to make API requests. The session is automatically closed after the test, releasing resources.
import pytest
import requests
@pytest.fixture
def api_client():
base_url = 'https://api.example.com'
session = requests.Session()
session.headers.update({'Authorization': 'Bearer YOUR_API_KEY'})
yield session
session.close()
def test_get_user(api_client):
response = api_client.get('/users/123')
assert response.status_code == 200
assert response.json()['username'] == 'johndoe'
Fixture Scope
The `scope` parameter determines how often a fixture is created and destroyed. Common scopes are: 'function' (default): Fixture is created and destroyed for each test function. 'class': Fixture is created and destroyed once per class. 'module': Fixture is created and destroyed once per module. 'package': Fixture is created and destroyed once per package. 'session': Fixture is created and destroyed once per test session (all tests). In this example, the `global_resource` fixture has `scope='session'`, meaning it's created only once at the beginning of the test session and destroyed at the end. This is useful for resources that are expensive to create and can be shared across multiple tests. The output confirms that resource is created only once, used in two tests, and destroyed after the tests are completed.
import pytest
@pytest.fixture(scope='session')
def global_resource():
# Setup: Create a resource that should only be created once per test session
resource = create_expensive_resource()
print("\nGlobal resource created!")
yield resource
# Teardown: Clean up the resource
resource.cleanup()
print("\nGlobal resource cleaned up!")
def create_expensive_resource():
# Simulating an expensive resource creation
print("Creating an expensive resource...")
return type('ExpensiveResource', (object,), { 'is_ready': True, 'cleanup': lambda self: print('Cleaning up...') })()
def test_use_resource_1(global_resource):
assert global_resource.is_ready == True
print("\nTest 1 using global resource...")
def test_use_resource_2(global_resource):
assert global_resource.is_ready == True
print("\nTest 2 using global resource...")
Best Practices
Interview Tip
When discussing fixtures in an interview, be prepared to explain:
When to Use Fixtures
Use fixtures whenever you have setup or teardown logic that needs to be shared across multiple tests. Common scenarios include:
Memory Footprint
Fixtures can impact memory usage, especially when using wider scopes like 'session'. Be mindful of the size of the resources you're creating in fixtures and consider using a smaller scope if the resource is not needed for the entire test session. Ensure proper teardown to release resources promptly.
Alternatives
While fixtures are generally the preferred approach in pytest, alternatives exist:
Pros
Cons
FAQ
-
How do I pass parameters to a fixture?
You can use the `request` fixture to access information about the test context, including parameters passed via the command line or using `pytest.mark.parametrize`. See the pytest documentation for examples. -
What is a `conftest.py` file?
A `conftest.py` file is a special file that pytest automatically recognizes. You can define fixtures in `conftest.py` to make them available to all tests in the directory where the file is located and its subdirectories. This is a convenient way to share fixtures across multiple test files. -
Can I override fixtures?
Yes, you can override fixtures by defining a fixture with the same name in a more specific scope (e.g., in a test file instead of `conftest.py`). The fixture in the more specific scope will take precedence.