| name | designing-tests |
| description | Guides test strategy, TDD/BDD approaches, test coverage planning, and testing best practices. Use when designing test suites, improving coverage, or choosing testing approaches. |
| license | MIT |
| compatibility | opencode |
| metadata | {"category":"quality","audience":"developers"} |
Designing Tests
Strategies and patterns for designing effective, maintainable test suites.
When to Use This Skill
- Planning test coverage for new features
- Choosing between testing approaches (TDD, BDD)
- Designing integration or E2E tests
- Improving existing test suites
- Setting up testing infrastructure
- Debugging flaky tests
The Testing Pyramid
โโโโโโโโโโโ
โ E2E โ โ Few, slow, expensive
โ Tests โ (Selenium, Playwright)
โโโโโโโโโโโค
โ โ
โโโโค Integr- โ โ Some, medium speed
โ โ ation โ (API tests, DB tests)
โ โ Tests โ
โ โโโโโโโโโโโค
โ โ โ
โ โ Unit โ โ Many, fast, cheap
โ โ Tests โ (Pure functions, isolated)
โโโโดโโโโโโโโโโ
| Level | Speed | Scope | Quantity | Purpose |
|---|
| Unit | ~ms | Single function/class | Many (70-80%) | Logic correctness |
| Integration | ~s | Multiple components | Some (15-20%) | Component interaction |
| E2E | ~10s+ | Full system | Few (5-10%) | User flows work |
Test-Driven Development (TDD)
The Red-Green-Refactor Cycle
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ โ
โผ โ
โโโโโโโโโโโ โโโโโโโโโโโ โโโโโโโโโโดโโโ
โ RED โโโโโถโ GREEN โโโโโถโ REFACTOR โ
โ Write โ โ Make โ โ Clean โ
โ failing โ โ it โ โ up โ
โ test โ โ pass โ โ code โ
โโโโโโโโโโโ โโโโโโโโโโโ โโโโโโโโโโโโโ
TDD Best Practices
- Write the test first - Don't write production code without a failing test
- Write the minimal test - One behavior per test
- Write the minimal code - Just enough to pass
- Refactor ruthlessly - Clean up after green
- Run tests frequently - After every small change
TDD Example Flow
def test_calculate_total_with_discount():
order = Order(items=[Item(price=100)])
order.apply_discount(10)
assert order.total() == 90
class Order:
def __init__(self, items):
self.items = items
self.discount = 0
def apply_discount(self, percent):
self.discount = percent
def total(self):
subtotal = sum(i.price for i in self.items)
return subtotal * (100 - self.discount) / 100
Behavior-Driven Development (BDD)
Gherkin Syntax
Feature: Shopping Cart
As a customer
I want to add items to my cart
So that I can purchase them later
Scenario: Add item to empty cart
Given I have an empty cart
When I add a product "Widget" priced at $10
Then my cart should contain 1 item
And my cart total should be $10
Scenario: Apply discount code
Given I have a cart with total $100
When I apply discount code "SAVE10"
Then my cart total should be $90
BDD Benefits
- Tests as documentation
- Shared language with stakeholders
- Focus on behavior, not implementation
- Easy to understand test intent
Test Design Patterns
Arrange-Act-Assert (AAA)
def test_user_registration():
user_data = {"email": "test@example.com", "password": "secure123"}
user_service = UserService(mock_repository)
result = user_service.register(user_data)
assert result.success is True
assert result.user.email == "test@example.com"
Given-When-Then (BDD style)
def test_order_cancellation():
order = create_confirmed_order()
order.cancel()
assert order.status == "cancelled"
assert order.refund_initiated is True
Test Data Builders
class UserBuilder:
def __init__(self):
self.email = "default@test.com"
self.name = "Test User"
self.role = "user"
def with_email(self, email):
self.email = email
return self
def with_role(self, role):
self.role = role
return self
def build(self):
return User(email=self.email, name=self.name, role=self.role)
admin = UserBuilder().with_role("admin").build()
Object Mother Pattern
class TestUsers:
@staticmethod
def admin():
return User(email="admin@test.com", role="admin")
@staticmethod
def customer():
return User(email="customer@test.com", role="customer")
@staticmethod
def guest():
return User(email=None, role="guest")
Mocking Strategies
When to Mock
| Mock | Don't Mock |
|---|
| External APIs | Pure business logic |
| Database (for unit tests) | Simple value objects |
| File system | Deterministic functions |
| Time/random | Core domain entities |
| Third-party services | Internal collaborators (usually) |
Mock Types
| Type | Purpose | Example |
|---|
| Stub | Return canned responses | mock.return_value = 42 |
| Mock | Verify interactions | mock.assert_called_with(...) |
| Spy | Track real calls | Wraps real object, records calls |
| Fake | Simplified implementation | In-memory database |
Mocking Example
from unittest.mock import Mock, patch
def test_send_email_on_registration():
mock_email_service = Mock()
user_service = UserService(email_service=mock_email_service)
user_service.register({"email": "test@example.com"})
mock_email_service.send_welcome_email.assert_called_once_with("test@example.com")
@patch("app.services.EmailService")
def test_with_patch(mock_email_class):
mock_email_class.return_value.send.return_value = True
Integration Test Patterns
Database Tests
import pytest
from testcontainers.postgres import PostgresContainer
@pytest.fixture(scope="session")
def database():
with PostgresContainer("postgres:15") as postgres:
yield postgres.get_connection_url()
def test_user_persistence(database):
repo = UserRepository(database)
user = User(email="test@example.com")
repo.save(user)
retrieved = repo.find_by_email("test@example.com")
assert retrieved.email == user.email
API Tests
def test_create_user_endpoint(client):
response = client.post("/api/users", json={
"email": "new@example.com",
"password": "secure123"
})
assert response.status_code == 201
assert response.json["email"] == "new@example.com"
assert "id" in response.json
E2E Test Patterns
Page Object Model
class LoginPage:
def __init__(self, page):
self.page = page
self.email_input = page.locator("#email")
self.password_input = page.locator("#password")
self.submit_button = page.locator("button[type=submit]")
def login(self, email, password):
self.email_input.fill(email)
self.password_input.fill(password)
self.submit_button.click()
return DashboardPage(self.page)
def test_successful_login(page):
login_page = LoginPage(page)
dashboard = login_page.login("user@example.com", "password")
assert dashboard.welcome_message.is_visible()
E2E Best Practices
- Use stable selectors - data-testid, not CSS classes
- Wait for conditions - Not arbitrary sleeps
- Isolate test data - Each test gets fresh data
- Test critical paths - Happy paths, key user journeys
- Keep them fast - Parallelize, minimize scope
Test Coverage Strategy
What to Cover
| Priority | What | Why |
|---|
| High | Business logic | Core value |
| High | Edge cases | Where bugs hide |
| High | Error paths | Graceful failures |
| Medium | Integration points | Contract validation |
| Low | UI layout | Brittle, low value |
| Low | Third-party code | Not your responsibility |
Coverage Metrics
| Metric | Target | Notes |
|---|
| Line coverage | 70-80% | Basic minimum |
| Branch coverage | 60-70% | Catches conditionals |
| Mutation score | 50-70% | Measures test quality |
Meaningful Coverage
HIGH VALUE:
โ Core business logic
โ Data transformations
โ Error handling
โ Security-sensitive code
LOW VALUE:
โ Getters/setters
โ Constructor-only classes
โ Framework boilerplate
โ Configuration files
Handling Flaky Tests
Common Causes
| Cause | Solution |
|---|
| Timing issues | Use explicit waits, not sleep |
| Shared state | Isolate test data |
| External dependencies | Mock or use containers |
| Race conditions | Add synchronization |
| Date/time | Mock time providers |
| Random data | Seed random generators |
Flaky Test Checklist
Test Organization
File Structure
tests/
โโโ unit/ # Unit tests
โ โโโ services/
โ โ โโโ test_user_service.py
โ โโโ models/
โ โโโ test_order.py
โโโ integration/ # Integration tests
โ โโโ api/
โ โ โโโ test_user_endpoints.py
โ โโโ repositories/
โ โโโ test_user_repository.py
โโโ e2e/ # End-to-end tests
โ โโโ test_checkout_flow.py
โโโ fixtures/ # Shared fixtures
โ โโโ factories.py
โโโ conftest.py # Pytest configuration
Naming Conventions
def test_calculate_total_with_discount_returns_reduced_price():
pass
def test_login_with_invalid_password_raises_auth_error():
pass
def test_order_when_cancelled_sends_refund_notification():
pass
Anti-Patterns to Avoid
- Testing implementation, not behavior - Tests break on refactor
- Large test methods - Hard to debug, unclear intent
- Excessive mocking - Tests don't reflect reality
- Shared mutable state - Tests affect each other
- Ignoring test failures - Broken windows effect
- Testing private methods - Coupling to implementation
- No assertion - Tests that can't fail
- Copy-paste tests - Maintenance nightmare
Quick Reference
PYRAMID:
Unit (70%) โ Integration (20%) โ E2E (10%)
TDD CYCLE:
Red โ Green โ Refactor
PATTERNS:
AAA: Arrange-Act-Assert
Builder: Fluent test data creation
Page Object: E2E abstraction
MOCK WHEN:
External APIs, Database (unit), Time, Random
COVERAGE:
70-80% line, focus on business logic
NAMING:
test_[what]_[condition]_[expected]