DRY in Testing
When to Use
When writing test suites and encountering repeated setup, teardown, or assertion patterns across tests.
Decision Framework
| If you have... | Use | Why |
|---|---|---|
| Repeated setup/teardown | Fixtures, setUp/tearDown | Eliminate duplication, ensure consistency |
| Repeated test data creation | Factories, Builders | Generate realistic test data programmatically |
| Repeated assertions | Custom matchers, assertion helpers | Make tests more readable, DRY |
| Similar test structure | Parameterized tests | Test multiple inputs with same logic |
| Shared mocks/stubs | Mock factories, fixture modules | Reuse test doubles consistently |
| Repeated integration setup | Test harness, test containers | Standardize environment setup |
Pattern: Test Fixtures
# BEFORE: Duplicated setup in every test
class TestUserService(unittest.TestCase):
def test_create_user(self):
db = Database('sqlite:///:memory:')
db.migrate()
service = UserService(db)
# Test logic...
db.close()
def test_update_user(self):
db = Database('sqlite:///:memory:')
db.migrate()
service = UserService(db)
# Test logic...
db.close()
def test_delete_user(self):
db = Database('sqlite:///:memory:')
db.migrate()
service = UserService(db)
# Test logic...
db.close()
# AFTER: DRY with fixtures
class TestUserService(unittest.TestCase):
def setUp(self):
"""Runs before each test."""
self.db = Database('sqlite:///:memory:')
self.db.migrate()
self.service = UserService(self.db)
def tearDown(self):
"""Runs after each test."""
self.db.close()
def test_create_user(self):
# Just test logic — setup is DRY
user = self.service.create_user('test@example.com', 'password')
self.assertEqual(user.email, 'test@example.com')
def test_update_user(self):
user = self.service.create_user('test@example.com', 'password')
updated = self.service.update_user(user.id, email='new@example.com')
self.assertEqual(updated.email, 'new@example.com')
Pattern: Factories for Test Data
// ANTI-PATTERN: Repeated object creation
test('creates order with items', () => {
const user = {
id: 1,
email: 'test@example.com',
name: 'Test User',
createdAt: new Date(),
};
const order = { id: 1, userId: user.id, total: 100, status: 'pending' };
// Test...
});
test('processes order payment', () => {
const user = {
id: 1,
email: 'test@example.com',
name: 'Test User',
createdAt: new Date(),
};
const order = { id: 1, userId: user.id, total: 100, status: 'pending' };
// Test...
});
// DRY: Factory pattern
class UserFactory {
static create(overrides = {}) {
return {
id: Math.floor(Math.random() * 1000),
email: 'test@example.com',
name: 'Test User',
createdAt: new Date(),
...overrides,
};
}
}
class OrderFactory {
static create(overrides = {}) {
return {
id: Math.floor(Math.random() * 1000),
userId: 1,
total: 100,
status: 'pending',
...overrides,
};
}
}
test('creates order with items', () => {
const user = UserFactory.create();
const order = OrderFactory.create({ userId: user.id });
// Test is now focused on behavior, not data setup
});
test('processes high-value order', () => {
const user = UserFactory.create({ email: 'vip@example.com' });
const order = OrderFactory.create({ userId: user.id, total: 10000 });
// Easy to create variations
});
Pattern: Parameterized Tests
# ANTI-PATTERN: Repeated test structure
def test_discount_for_regular_customer():
price = calculate_discount(100, 'regular')
assert price == 100
def test_discount_for_vip_customer():
price = calculate_discount(100, 'vip')
assert price == 80
def test_discount_for_gold_customer():
price = calculate_discount(100, 'gold')
assert price == 90
# DRY: Parameterized test
import pytest
@pytest.mark.parametrize('customer_type,expected_price', [
('regular', 100),
('vip', 80),
('gold', 90),
])
def test_discount_calculation(customer_type, expected_price):
price = calculate_discount(100, customer_type)
assert price == expected_price
Pattern: Custom Assertions
// ANTI-PATTERN: Repeated assertion logic
test('validates email format', () => {
const result = validateEmail('test@example.com');
expect(result.valid).toBe(true);
expect(result.errors).toEqual([]);
expect(result.warnings).toEqual([]);
});
test('validates phone format', () => {
const result = validatePhone('555-1234');
expect(result.valid).toBe(true);
expect(result.errors).toEqual([]);
expect(result.warnings).toEqual([]);
});
// DRY: Custom matcher
expect.extend({
toBeValidValidationResult(received) {
const pass = received.valid === true
&& received.errors.length === 0
&& received.warnings.length === 0;
return {
pass,
message: () => `Expected validation result to be valid, got ${JSON.stringify(received)}`,
};
},
});
test('validates email format', () => {
expect(validateEmail('test@example.com')).toBeValidValidationResult();
});
test('validates phone format', () => {
expect(validatePhone('555-1234')).toBeValidValidationResult();
});
When NOT to DRY Tests
Keep duplication when:
- Tests become harder to understand with abstraction
- Each test covers fundamentally different scenarios
- Abstraction hides important test details
- Test readability > test code DRY
Dan North's principle: "Duplication is far cheaper than the wrong abstraction" applies even more to tests -- test clarity is paramount.
Common Mistakes
- Over-abstracting tests — Tests become unreadable, hard to debug when they fail
- Shared state between tests — Tests become order-dependent, fail non-deterministically
- Magic fixtures — Test setup so abstracted that readers can't understand test context
- DRY assertions over clarity — Custom matchers that hide what's being tested
- Not resetting state — Fixtures leak state across tests