Making Tests Reusable with Fixtures¶
As you write more tests, you'll notice yourself copying the same setup code over and over. Fixtures solve this problem by letting you define setup once and reuse it everywhere.
The Problem: Repetitive Setup¶
Imagine you're testing a shopping cart:
def test_add_item():
cart = ShoppingCart() # Same setup
cart.add_item("Apple", 1.50)
assert cart.total == 1.50
def test_remove_item():
cart = ShoppingCart() # Same setup again
cart.add_item("Apple", 1.50)
cart.remove_item("Apple")
assert cart.total == 0.00
def test_multiple_items():
cart = ShoppingCart() # And again...
cart.add_item("Apple", 1.50)
cart.add_item("Banana", 0.75)
assert cart.total == 2.25
See the pattern? Every test creates a ShoppingCart(). This is repetitive and annoying.
The Solution: Fixtures¶
A fixture is a reusable piece of setup code:
from rustest import fixture
@fixture
def cart():
return ShoppingCart()
def test_add_item(cart):
cart.add_item("Apple", 1.50)
assert cart.total == 1.50
def test_remove_item(cart):
cart.add_item("Apple", 1.50)
cart.remove_item("Apple")
assert cart.total == 0.00
def test_multiple_items(cart):
cart.add_item("Apple", 1.50)
cart.add_item("Banana", 0.75)
assert cart.total == 2.25
What happened?
- We defined
cartas a fixture using@fixture - Each test function accepts
cartas a parameter - Rustest automatically calls the fixture and passes the result to your test
No more repetitive setup! 🎉
How Fixtures Work¶
When you run a test that uses a fixture:
- Rustest sees the test needs the
cartfixture - Rustest calls the
cart()function - Rustest passes the result to your test function
- Your test runs with the cart
It's like automatic dependency injection!
Fixture Benefits¶
✅ Less Code Duplication¶
Define setup once, use it everywhere:
@fixture
def database():
db = Database()
db.connect()
return db
# Now every test can use database without repeating setup
def test_insert_user(database):
database.insert("users", {"name": "Alice"})
assert database.count("users") == 1
def test_query_users(database):
database.insert("users", {"name": "Alice"})
users = database.query("users")
assert len(users) == 1
✅ Easier Maintenance¶
Change setup in one place, all tests update:
@fixture
def database():
# Changed from SQLite to PostgreSQL?
# Update it here, and all tests still work!
db = PostgresDatabase()
db.connect("test_db")
return db
✅ Clearer Tests¶
Tests focus on what they're testing, not setup details:
def test_user_login(database, user):
# The test is clear: we're testing login
result = login(user.email, user.password)
assert result.success is True
Real-World Example: Testing an API¶
Let's test an API client:
from rustest import fixture
@fixture
def api_client():
client = APIClient("https://api.example.com")
client.authenticate("test_token")
return client
def test_get_user(api_client):
user = api_client.get("/users/1")
assert user["name"] == "Alice"
def test_create_post(api_client):
post = api_client.post("/posts", {"title": "Hello World"})
assert post["id"] is not None
def test_delete_resource(api_client):
result = api_client.delete("/posts/123")
assert result.success is True
Every test gets a fresh, authenticated API client without any setup code!
Cleanup with Yield Fixtures¶
Sometimes you need to clean up after tests (close files, disconnect from databases, etc.). Use yield:
@fixture
def temp_file():
# SETUP: Create a file
file = open("test.txt", "w")
file.write("test data")
file.close()
# PROVIDE: Give the filename to the test
yield "test.txt"
# CLEANUP: Delete the file after the test
import os
os.remove("test.txt")
def test_read_file(temp_file):
with open(temp_file, "r") as f:
content = f.read()
assert content == "test data"
# After this test, temp_file is automatically deleted!
How it works:
- Code before
yieldruns before the test - The value after
yieldis passed to the test - Code after
yieldruns after the test (cleanup)
This ensures cleanup always happens, even if the test fails!
Built-in Fixtures¶
Rustest provides useful fixtures out of the box:
tmp_path: Temporary Directory¶
def test_create_file(tmp_path):
# tmp_path is a Path object to a temporary directory
file = tmp_path / "test.txt"
file.write_text("hello world")
assert file.read_text() == "hello world"
# Directory is automatically cleaned up after the test!
monkeypatch: Modify Things Temporarily¶
def test_with_env_var(monkeypatch):
# Set an environment variable just for this test
monkeypatch.setenv("API_KEY", "test_key_123")
# Your code that reads API_KEY will see "test_key_123"
assert os.getenv("API_KEY") == "test_key_123"
# After the test, the environment is restored!
capsys: Capture Printed Output¶
def test_print_message(capsys):
print("Hello, World!")
captured = capsys.readouterr()
assert captured.out == "Hello, World!\n"
Fixtures Can Use Other Fixtures¶
Fixtures can depend on other fixtures:
@fixture
def database():
db = Database()
db.connect()
yield db
db.disconnect()
@fixture
def user(database):
# This fixture uses the database fixture!
user = database.create_user("alice@example.com")
return user
def test_user_posts(database, user):
# This test uses both fixtures
post = database.create_post(user, "Hello World")
assert post.author == user
Rustest automatically resolves dependencies and runs fixtures in the right order.
Common Patterns¶
Fixture for Test Data¶
@fixture
def sample_users():
return [
{"name": "Alice", "email": "alice@example.com"},
{"name": "Bob", "email": "bob@example.com"},
]
def test_import_users(sample_users, database):
database.import_users(sample_users)
assert database.count("users") == 2
Fixture for Configuration¶
@fixture
def test_config():
return {
"debug": True,
"database_url": "sqlite:///test.db",
"api_key": "test_key",
}
def test_app_startup(test_config):
app = create_app(test_config)
assert app.is_debug is True
Fixture for Mocks¶
from rustest import fixture
@fixture
def mock_email_service(monkeypatch):
sent_emails = []
def fake_send_email(to, subject, body):
sent_emails.append({"to": to, "subject": subject})
monkeypatch.setattr("email.send", fake_send_email)
return sent_emails
def test_signup_sends_email(mock_email_service):
signup("alice@example.com", "password")
assert len(mock_email_service) == 1
assert mock_email_service[0]["subject"] == "Welcome!"
When to Use Fixtures¶
Use fixtures when you:
- ✅ Have the same setup in multiple tests
- ✅ Need to clean up resources (files, connections, etc.)
- ✅ Want to share test data across tests
- ✅ Need complex setup that would clutter your tests
Don't use fixtures when:
- ❌ The setup is used in only one test (just put it in the test)
- ❌ The fixture would be more confusing than helpful
What's Next?¶
Fixtures make your tests cleaner and more maintainable. Next, learn how to test the same logic with many different inputs:
Testing Multiple Cases (Parametrization)
Or explore how to organize larger test suites:
Want to dive deeper into fixtures?