Test Fixtures: setUp and tearDown¶
Time commitment: 20–25 minutes
What you'll learn:
- What test fixtures are and why they're useful
- How to use
setUp()andtearDown()for instance-level fixtures - How to use
setUpClass()andtearDownClass()for class-level fixtures - When to use module-level fixtures
- Best practices for writing clean, maintainable tests
Prerequisites:
- Completed Your First Test
- Completed Testing Thoroughly
- Basic understanding of classes and objects in Python
What are test fixtures?¶
A test fixture is the preparation needed to run tests. This includes:
- Creating objects or data structures
- Opening files or database connections
- Setting up temporary directories
- Configuring mock objects
Test fixtures ensure each test starts in a known, clean state.
The problem: Repetitive setup code¶
Without fixtures, you might write code like this:
import unittest
class ShoppingCart:
"""A simple shopping cart to demonstrate fixtures."""
def __init__(self):
self.items = []
def add_item(self, item, price):
"""Add an item to the cart."""
self.items.append({"item": item, "price": price})
def total(self):
"""Calculate the total cost of all items."""
return sum(item["price"] for item in self.items)
def clear(self):
"""Remove all items from the cart."""
self.items = []
class TestShoppingCartWithoutFixtures(unittest.TestCase):
"""Tests with repetitive setup - not using fixtures."""
def test_add_single_item(self):
"""Test adding a single item to the cart."""
cart = ShoppingCart() # Repetitive!
cart.add_item("Apple", 0.99)
self.assertEqual(len(cart.items), 1)
def test_calculate_total(self):
"""Test calculating the total cost."""
cart = ShoppingCart() # Repetitive!
cart.add_item("Apple", 0.99)
cart.add_item("Banana", 0.59)
self.assertEqual(cart.total(), 1.58)
def test_clear_cart(self):
"""Test clearing the cart."""
cart = ShoppingCart() # Repetitive!
cart.add_item("Apple", 0.99)
cart.clear()
self.assertEqual(len(cart.items), 0)
Notice how we create a new ShoppingCart() in every test method? This is repetitive and violates the DRY (Don't Repeat Yourself) principle.
Using setUp() to eliminate repetition¶
The setUp() method runs before each test method. Use it to create objects or prepare data that every test needs.
class TestShoppingCartWithSetUp(unittest.TestCase):
"""Tests using setUp() to prepare fixtures."""
def setUp(self):
"""Run before each test method - creates a fresh cart."""
self.cart = ShoppingCart()
print(f"setUp: Created new cart (id: {id(self.cart)})")
def test_add_single_item(self):
"""Test adding a single item to the cart."""
self.cart.add_item("Apple", 0.99)
self.assertEqual(len(self.cart.items), 1)
print(f"test_add_single_item: Cart has {len(self.cart.items)} item(s)")
def test_calculate_total(self):
"""Test calculating the total cost."""
self.cart.add_item("Apple", 0.99)
self.cart.add_item("Banana", 0.59)
self.assertEqual(self.cart.total(), 1.58)
print(f"test_calculate_total: Total is £{self.cart.total():.2f}")
def test_clear_cart(self):
"""Test clearing the cart."""
self.cart.add_item("Apple", 0.99)
self.cart.clear()
self.assertEqual(len(self.cart.items), 0)
print(f"test_clear_cart: Cart cleared, now has {len(self.cart.items)} item(s)")
Let's run these tests to see setUp() in action:
# Run the tests
suite = unittest.TestLoader().loadTestsFromTestCase(TestShoppingCartWithSetUp)
runner = unittest.TextTestRunner(verbosity=2)
runner.run(suite)
Key observations:
setUp()runs before each test method- Each test gets a fresh cart (notice different object IDs)
- Tests are isolated - changes in one test don't affect others
- The test code is cleaner - no repetitive cart creation
Using tearDown() to clean up¶
The tearDown() method runs after each test method, regardless of whether the test passed or failed. Use it to clean up resources like:
- Closing files
- Closing database connections
- Deleting temporary files
- Resetting global state
Here's a practical example with file operations:
import tempfile
import os
from pathlib import Path
class FileProcessor:
"""A simple file processor to demonstrate tearDown."""
def __init__(self, filepath):
self.filepath = filepath
def write_lines(self, lines):
"""Write lines to the file."""
with open(self.filepath, 'w') as f:
f.write('\n'.join(lines))
def read_lines(self):
"""Read lines from the file."""
with open(self.filepath, 'r') as f:
return [line.strip() for line in f.readlines()]
def count_lines(self):
"""Count the number of lines in the file."""
return len(self.read_lines())
class TestFileProcessor(unittest.TestCase):
"""Tests for FileProcessor with proper cleanup."""
def setUp(self):
"""Create a temporary file before each test."""
# Create a temporary file
self.temp_file = tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.txt')
self.temp_filepath = self.temp_file.name
self.temp_file.close()
# Create the processor
self.processor = FileProcessor(self.temp_filepath)
print(f"setUp: Created temp file at {self.temp_filepath}")
def tearDown(self):
"""Delete the temporary file after each test."""
if os.path.exists(self.temp_filepath):
os.remove(self.temp_filepath)
print(f"tearDown: Deleted temp file {self.temp_filepath}")
def test_write_and_read_lines(self):
"""Test writing and reading lines."""
lines = ["Hello", "World", "Testing"]
self.processor.write_lines(lines)
result = self.processor.read_lines()
self.assertEqual(result, lines)
def test_count_lines(self):
"""Test counting lines in a file."""
lines = ["Line 1", "Line 2", "Line 3", "Line 4"]
self.processor.write_lines(lines)
self.assertEqual(self.processor.count_lines(), 4)
def test_empty_file(self):
"""Test reading an empty file."""
self.processor.write_lines([])
result = self.processor.read_lines()
self.assertEqual(result, ['']) # Empty file has one empty line
Let's run these tests:
# Run the tests
suite = unittest.TestLoader().loadTestsFromTestCase(TestFileProcessor)
runner = unittest.TextTestRunner(verbosity=2)
runner.run(suite)
Key observations:
setUp()creates a temporary file before each testtearDown()always runs, even if a test fails- Each test gets its own fresh file
- No leftover files after tests complete
Class-level fixtures: setUpClass() and tearDownClass()¶
Sometimes setup is expensive (for example, database connections, loading large datasets). For these cases, use class-level fixtures that run once per test class.
setUpClass()runs once before any tests in the classtearDownClass()runs once after all tests in the class
These methods must be decorated with @classmethod:
class Database:
"""Mock database to demonstrate class-level fixtures."""
def __init__(self, db_name):
self.db_name = db_name
self.data = {}
print(f"Database '{db_name}' connected")
def insert(self, key, value):
"""Insert a key-value pair."""
self.data[key] = value
def get(self, key):
"""Get a value by key."""
return self.data.get(key)
def close(self):
"""Close the database connection."""
print(f"Database '{self.db_name}' closed")
self.data = None
class TestDatabaseWithClassFixtures(unittest.TestCase):
"""Tests using class-level fixtures for expensive setup."""
@classmethod
def setUpClass(cls):
"""Run once before all tests - create database connection."""
print("\nsetUpClass: Creating database connection...")
cls.db = Database("test_db")
# Load initial test data (imagine this is expensive)
cls.db.insert("user:1", {"name": "Alice", "role": "admin"})
cls.db.insert("user:2", {"name": "Bob", "role": "user"})
print("setUpClass: Database ready with test data\n")
@classmethod
def tearDownClass(cls):
"""Run once after all tests - close database connection."""
print("\ntearDownClass: Closing database connection...")
cls.db.close()
print("tearDownClass: Cleanup complete\n")
def setUp(self):
"""Run before each test - create a snapshot of current data."""
# Store initial state so we can verify tests don't affect each other
self.initial_data_count = len(self.db.data)
print(f"setUp: Database has {self.initial_data_count} records")
def test_get_user_alice(self):
"""Test retrieving Alice's record."""
user = self.db.get("user:1")
self.assertEqual(user["name"], "Alice")
self.assertEqual(user["role"], "admin")
print("test_get_user_alice: Retrieved Alice successfully")
def test_get_user_bob(self):
"""Test retrieving Bob's record."""
user = self.db.get("user:2")
self.assertEqual(user["name"], "Bob")
self.assertEqual(user["role"], "user")
print("test_get_user_bob: Retrieved Bob successfully")
def test_insert_new_user(self):
"""Test inserting a new user."""
self.db.insert("user:3", {"name": "Charlie", "role": "user"})
user = self.db.get("user:3")
self.assertEqual(user["name"], "Charlie")
print(f"test_insert_new_user: Database now has {len(self.db.data)} records")
Let's run these tests:
# Run the tests
suite = unittest.TestLoader().loadTestsFromTestCase(TestDatabaseWithClassFixtures)
runner = unittest.TextTestRunner(verbosity=2)
runner.run(suite)
Key observations:
setUpClass()runs once at the startsetUp()still runs before each test- All tests share the same database instance
tearDownClass()runs once at the end- The third test adds data that persists for the class
Warning: Class-level fixtures can lead to tests affecting each other if you're not careful. Use them only when:
- Setup is genuinely expensive (for example, database connections, loading large files)
- Tests only read from the shared resource
- Or you carefully reset state in
setUp()
Module-level fixtures¶
For even more expensive setup, use module-level fixtures:
def setUpModule():
"""Run once when the test module is loaded."""
# For example: start a test server, load a huge dataset
pass
def tearDownModule():
"""Run once when the test module finishes."""
# For example: stop the test server, clean up resources
pass
These run once per module (test file), shared across all test classes in that file.
Best practices for test fixtures¶
1. Keep setUp() simple and fast¶
# Good - simple and fast
def setUp(self):
self.cart = ShoppingCart()
self.user = {"name": "Alice", "id": 1}
# Avoid - slow operations in setUp()
def setUp(self):
self.api_client = connect_to_production_api() # Slow!
self.large_dataset = load_1gb_file() # Slow!
2. Each test should be independent¶
# Good - each test is independent
def setUp(self):
self.cart = ShoppingCart()
def test_add_item(self):
self.cart.add_item("Apple", 0.99)
self.assertEqual(len(self.cart.items), 1)
def test_total(self):
# Starts with a fresh cart - doesn't depend on test_add_item
self.cart.add_item("Banana", 0.59)
self.assertEqual(self.cart.total(), 0.59)
# Avoid - tests depending on each other
def test_add_item(self):
self.cart.add_item("Apple", 0.99)
def test_total(self):
# BAD: Assumes test_add_item ran first!
self.assertEqual(self.cart.total(), 0.99)
3. Always clean up in tearDown()¶
# Good - reliable cleanup
def tearDown(self):
if os.path.exists(self.temp_file):
os.remove(self.temp_file)
if self.db_connection and self.db_connection.is_connected():
self.db_connection.close()
# Avoid - forgetting to clean up
def tearDown(self):
pass # Oops, temp files will accumulate!
4. Use class fixtures sparingly¶
# Good use of class fixtures - expensive, read-only setup
@classmethod
def setUpClass(cls):
cls.large_dataset = load_100mb_reference_data() # Expensive, read-only
# Questionable - mutable shared state
@classmethod
def setUpClass(cls):
cls.cart = ShoppingCart() # Tests will modify this!
5. Name your fixtures clearly¶
# Good - clear fixture names
def setUp(self):
self.empty_cart = ShoppingCart()
self.admin_user = User("admin", role="admin")
self.test_data_file = "/tmp/test_data.json"
# Avoid - vague names
def setUp(self):
self.obj = ShoppingCart()
self.thing = User("admin")
self.file = "/tmp/test_data.json"
Hands-on exercise: Bank account with fixtures¶
Let's create a bank account class and write tests using fixtures.
Your task:
- Complete the test class below
- Add
setUp()to create a fresh account before each test - Write tests for deposit, withdraw, and balance
- Verify tests are independent
class BankAccount:
"""A simple bank account for the exercise."""
def __init__(self, initial_balance=0):
self.balance = initial_balance
self.transaction_count = 0
def deposit(self, amount):
"""Deposit money into the account."""
if amount <= 0:
raise ValueError("Deposit amount must be positive")
self.balance += amount
self.transaction_count += 1
def withdraw(self, amount):
"""Withdraw money from the account."""
if amount <= 0:
raise ValueError("Withdrawal amount must be positive")
if amount > self.balance:
raise ValueError("Insufficient funds")
self.balance -= amount
self.transaction_count += 1
def get_balance(self):
"""Get the current balance."""
return self.balance
# TODO: Complete this test class
class TestBankAccount(unittest.TestCase):
"""Tests for BankAccount using fixtures."""
def setUp(self):
"""Create a fresh bank account before each test."""
# TODO: Create a BankAccount with £100 initial balance
pass
def test_initial_balance(self):
"""Test that the account starts with the correct balance."""
# TODO: Assert that balance is £100
pass
def test_deposit(self):
"""Test depositing money."""
# TODO: Deposit £50 and verify balance is £150
pass
def test_withdraw(self):
"""Test withdrawing money."""
# TODO: Withdraw £30 and verify balance is £70
pass
def test_withdraw_insufficient_funds(self):
"""Test that withdrawing too much raises an error."""
# TODO: Attempt to withdraw £200 and verify ValueError is raised
pass
def test_multiple_transactions(self):
"""Test multiple deposits and withdrawals."""
# TODO: Deposit £50, withdraw £30, deposit £20
# Verify final balance is £140
# Verify transaction_count is 3
pass
Solution¶
Here's the completed test class:
class TestBankAccountSolution(unittest.TestCase):
"""Tests for BankAccount using fixtures - SOLUTION."""
def setUp(self):
"""Create a fresh bank account before each test."""
self.account = BankAccount(initial_balance=100)
def test_initial_balance(self):
"""Test that the account starts with the correct balance."""
self.assertEqual(self.account.get_balance(), 100)
def test_deposit(self):
"""Test depositing money."""
self.account.deposit(50)
self.assertEqual(self.account.get_balance(), 150)
def test_withdraw(self):
"""Test withdrawing money."""
self.account.withdraw(30)
self.assertEqual(self.account.get_balance(), 70)
def test_withdraw_insufficient_funds(self):
"""Test that withdrawing too much raises an error."""
with self.assertRaises(ValueError) as cm:
self.account.withdraw(200)
self.assertEqual(str(cm.exception), "Insufficient funds")
def test_multiple_transactions(self):
"""Test multiple deposits and withdrawals."""
self.account.deposit(50) # 100 + 50 = 150
self.account.withdraw(30) # 150 - 30 = 120
self.account.deposit(20) # 120 + 20 = 140
self.assertEqual(self.account.get_balance(), 140)
self.assertEqual(self.account.transaction_count, 3)
Let's run the solution tests:
# Run the tests
suite = unittest.TestLoader().loadTestsFromTestCase(TestBankAccountSolution)
runner = unittest.TextTestRunner(verbosity=2)
runner.run(suite)
Summary¶
You've learnt how to use test fixtures to write cleaner, more maintainable tests:
Key concepts¶
| Fixture Type | Runs | Use For |
|---|---|---|
setUp() |
Before each test method | Creating fresh objects, opening files |
tearDown() |
After each test method | Closing files, cleaning up resources |
setUpClass() |
Once before all tests in class | Expensive setup (database connections) |
tearDownClass() |
Once after all tests in class | Closing shared resources |
setUpModule() |
Once when module loads | Very expensive setup (test servers) |
tearDownModule() |
Once when module finishes | Cleaning up module-level resources |
Best practices¶
- Use
setUp()to eliminate repetitive code - create fresh objects for each test - Always clean up in
tearDown()- close files, connections, delete temp files - Keep tests independent - don't rely on test execution order
- Use class fixtures sparingly - only for genuinely expensive, read-only setup
- Name fixtures clearly -
self.empty_cartis better thanself.cart
Common pitfalls¶
- Forgetting
@classmethodonsetUpClass()andtearDownClass() - Slow operations in
setUp()- runs before every test, making tests slow - Mutable shared state in class fixtures - tests can affect each other
- Not cleaning up in
tearDown()- temporary files and connections accumulate
Next steps¶
- Run Tests in Jupyter: Learn how to run your tests effectively
- Test Exceptions: Learn how to test error conditions
- Avoid Common Mistakes: Learn common testing pitfalls
- Assertions Reference: Complete guide to all unittest assertions
Further practice¶
Try these exercises to reinforce your learning:
File manager tests: Create a class that manages files in a directory, using
setUp()to create a temp directory andtearDown()to delete itUser session tests: Create a user session class with login/logout, using fixtures to ensure each test starts logged out
Configuration tests: Create a configuration loader that reads from a file, using class fixtures to load configuration once
API client tests: Create a mock API client, using
setUpClass()to "connect" once andsetUp()to reset the client state before each test