What is a test fixture?
A test fixture is the known state a test needs before it runs, plus the code that creates that state and removes it afterward. The state can be data or an environment, e.g. a test database that holds one customer with 2 items in the cart. A fixture makes a test repeatable, because each run starts from the same state.
The International Software Testing Qualifications Board (ISTQB) defines a test fixture as the predefined data and test environment that let software be tested in a repeatable way. In pytest, a fixture is the function that builds the state and hands it to the test. In Ruby on Rails and Django, a fixture is a file of database records that loads before tests run.
In software testing, a fixture is one part of a test harness, the code that runs tests and checks their results. A fixture differs from a test double, e.g. a mock, which replaces a component that the code under test calls. A fixture can still build a mock and hand it to a test. In electronics manufacturing, "test fixture" also names a physical device that holds a circuit board while it is tested.
How do setup and teardown work?
Setup is the step that builds a fixture, and teardown is the step that removes it. A test runner puts each test between the two steps:
- The runner runs the setup code, e.g. code that adds a customer to a test database.
- The test runs the code under test from that state and checks the result.
- The runner runs the teardown code, which removes what setup built, whether the test passed or failed.
Setup is the arrange step of arrange-act-assert, moved out of the test so several tests can reuse it. Test frameworks name the two steps differently:
| Framework | Setup | Teardown |
|---|---|---|
| pytest | Fixture code before yield | Fixture code after yield |
Python unittest | setUp method | tearDown method |
| JUnit 5 | @BeforeEach method | @AfterEach method |
| Jest | beforeEach function | afterEach function |
| Playwright Test | Fixture code before await use() | Fixture code after await use() |
In pytest, a test requests a fixture by naming it as a parameter. A fixture in a conftest.py file is available to the tests in that folder and below it. After a test, pytest runs teardowns in the reverse order of their setups.
A fixture's scope sets how often setup and teardown run. The default pytest scope, function, builds a fresh fixture for each test. The class, module, package, and session scopes share one fixture across a class, a file, a package, or the whole run, which saves setup time. A unit test usually needs a small fixture in memory, while an integration test often needs a database with known rows.
What is an example of a test fixture?
Here is an illustrative example. Acme Co. sells furniture online. A developer asks a coding agent to "Let customers edit their delivery address during checkout." The agent changes the checkout code. Acme keeps a fixture file, tests/fixtures/cart.json, that holds the cart each checkout test starts with:
[
{"sku": "oak-chair", "quantity": 1, "price": "120.00"},
{"sku": "oak-stool", "quantity": 1, "price": "120.00"}
]
A pytest fixture loads that file for a fresh customer, and the test checks the address change:
import json
import pytest
@pytest.fixture
def customer(db):
c = db.create_customer(email="buyer@example.com")
with open("tests/fixtures/cart.json") as f:
c.cart.load(json.load(f)) # 2 items, $240.00 in all
yield c
db.delete_customer(c.id) # teardown
def test_edit_address_keeps_cart(customer):
customer.checkout.set_address("12 Elm Street")
assert customer.address == "12 Elm Street"
assert len(customer.cart.items) == 2
The run goes through these steps:
- pytest reads the test's parameters and finds
customer, which in turn requests adbfixture fromconftest.py. - The
dbfixture connects to the test database. - The
customerfixture creates a customer and loads the 2 items from the fixture file. - The test changes the address to 12 Elm Street, and the address check passes.
- The cart check fails. The address saved, but the cart emptied.
- pytest still runs the code after
yield, which deletes the customer, and then thedbfixture closes its connection. - The next test starts with no customer left over from this one.
pytest prints the values that differed:
> assert len(customer.cart.items) == 2
E assert 0 == 2
The fixture makes the cart check possible. Because each run starts with exactly 2 items, the test can expect exactly 2 afterward. This example is simplified. A real suite would give each test its own email address, so tests that run in parallel do not collide.
What changes when a coding agent writes the code?
When a coding agent writes the code, the tests, and their fixtures in one session, it generates all three from the same prompt and context. The fixture can then leave out the case the code gets wrong. Suppose the agent's fixture held an empty cart, and its test checked that the cart keeps its items. That test would pass on the broken code, with 0 items before and after.
An empty cart can reach a test even when a shared fixture in conftest.py loads 2 items. In pytest, a fixture defined in a test file overrides a conftest.py fixture of the same name for the tests in that file. If an agent adds its own customer fixture to its test file, those tests still request customer but start from the agent's state. The test functions match the rest of the suite, so a reviewer who reads only them can miss the change.
A team can review an added or changed fixture as closely as a changed check, and ask why a test file redefines a fixture from conftest.py.
How do fixtures cause flaky tests?
A fixture causes a flaky test when a test's starting state changes between runs of the same code. Four common mistakes cause that change:
- Teardown that never runs. In pytest, a fixture that fails before its
yieldskips its teardown, so what it built stays. A killed test process runs no teardown, so the next Acme run can fail to create a customer whose email is still stored. - A shared fixture that tests change. A test that edits a fixture with session scope changes the start of each later test that uses it. Results depend on test order, which breaks test isolation.
- Values that change on each run. A fixture that reads the clock or generates random data, e.g. an order date, gives each run a different start.
- State the fixture never made. A test can pass on rows left in a laptop's database and fail on a fresh runner in continuous integration (CI). That is one reason tests pass locally but fail in CI.
A fixture also checks nothing by itself. A test with a careful fixture and a weak check still passes on broken code.
How is cleaning up after a test different from resetting before it?
Cleaning up after a test, the teardown, removes what setup built and what the test added. Resetting before a test restores a known starting state, whatever earlier tests left. Teardown keeps each test's changes inside that test, but a crash or a killed run skips it. A reset does not depend on earlier tests, but it costs time and can hide a test that leaves records behind.
The Cypress best practices recommend resetting state before tests, because an after hook may not run. Leftover state also shows a person where a failed test stopped.
A suite can do both. Each run starts by seeding a fresh database, and each test removes its own records. Wrapping each test in a database transaction that rolls back does both jobs in one step. The rollback works only when the code under test uses the test's connection and does not commit that transaction.
FAQs
What is a pytest fixture?
A pytest fixture is a function that pytest runs to build a state or an object for a test. A test requests the fixture by naming it as a parameter. Code after the fixture's yield statement runs as teardown when its scope ends, which for the default scope is when the test finishes, even if it fails.
What is the difference between a fixture and a mock?
A fixture is the starting state a test needs, e.g. a customer record in a test database. A mock is a kind of test double, which replaces a component that the code under test calls.
Should tests share fixtures?
Tests should share fixture code, but they should usually not share fixture state that they change. A fixture with function scope gives each test a fresh copy. A fixture with a wider scope saves setup time, but a change to it reaches later tests, so it suits data that no test edits.
What is a fixture file?
A fixture file is a file of fixed sample data that a test loads during setup, e.g. a JSON file that holds the items in a cart. In Ruby on Rails and Django, fixture files hold database records. A change to one changes the state that tests start from and deserves the same review as a changed check.