| name | test-writer |
| description | Use this skill when writing unit tests or integration tests for code. Prefers integration tests that reflect real usage, keeps tests fast and simple, and avoids over-mocking. Trigger after implementing features or when adding test coverage. |
Test Writer Protocol
Write tests that catch real bugs and run fast. Prefer integration tests. Keep it simple.
Core Principles
- Integration over unit - Test how things work together, not internals
- Real usage - Tests should look like how code is actually used
- Fast tests - Slow tests are a permanent tax on the repo
- Simple tests - If a test is hard to understand, it's hard to maintain
- Pragmatic coverage - High coverage when it makes sense, don't chase numbers
Test Hierarchy
Prefer tests in this order:
1. Integration tests (best)
- Test real components working together
- Use real database, real HTTP calls (to test servers)
- Catch the bugs that actually happen
2. Functional tests
- Test a module's public interface
- Real dependencies where reasonable
- Mock only external services
3. Unit tests (use sparingly)
- Only for complex pure logic
- Algorithms, calculations, parsing
- Avoid for simple glue code
Integration Test Philosophy
Test the whole flow:
async def test_create_order():
user = await create_test_user()
product = await create_test_product(price=100)
order = await order_service.create(user_id=user.id, product_id=product.id)
assert order.total == 100
assert await db.orders.exists(order.id)
assert user.balance_changed()
def test_create_order():
mock_db = Mock()
mock_user = Mock(id=1)
mock_product = Mock(id=2, price=100)
mock_db.orders.insert.return_value = Mock(id=99)
result = create_order(mock_db, mock_user, mock_product)
assert result.id == 99
When to Unit Test
Unit tests make sense for:
- Pure functions with complex logic
- Algorithms (sorting, parsing, calculations)
- Edge case handling in isolated functions
- Code that's genuinely independent
def test_calculate_discount():
assert calculate_discount(100, percent=10) == 90
assert calculate_discount(100, percent=0) == 100
assert calculate_discount(0, percent=50) == 0
def test_user_service_calls_repository():
mock_repo = Mock()
service = UserService(mock_repo)
service.get_user(1)
mock_repo.find.assert_called_with(1)
Keep Tests Fast
Speed budget:
- Unit test: < 10ms
- Integration test: < 500ms
- Full test suite: < 60 seconds (ideal), < 5 minutes (maximum)
How to stay fast:
@pytest.fixture
async def db():
async with test_database() as db:
yield db
Slow test red flags:
- Sleeping/waiting for time (
time.sleep, setTimeout)
- Starting/stopping servers per test
- Network calls to external services
- Large dataset generation
Fix slow tests:
def test_cache_expires():
cache.set("key", "value", ttl=60)
time.sleep(61)
assert cache.get("key") is None
def test_cache_expires(mock_time):
cache.set("key", "value", ttl=60)
mock_time.advance(61)
assert cache.get("key") is None
Keep Tests Simple
One concept per test:
def test_user_can_place_order():
...
def test_user_cannot_order_out_of_stock_product():
...
def test_order_calculates_tax_correctly():
...
def test_order_system():
Readable assertions:
assert user.is_active
assert order.total == 150
assert "error" not in response.json()
assert response.json()["data"]["user"]["status"]["active"] == True and \
response.json()["data"]["order"]["totals"]["final"] == 150
Minimal setup:
def test_discount_applied():
order = Order(subtotal=100)
order.apply_discount(10)
assert order.total == 90
def test_discount_applied():
user = User(name="John", email="john@example.com", created=datetime.now())
product = Product(name="Widget", sku="W001", category="Tools")
order = Order(user=user, products=[product], subtotal=100, ...)
What to Mock (and What Not To)
Mock:
- External APIs (payment providers, email services)
- Time-dependent behavior
- Random/non-deterministic operations
- Services you don't control
Don't mock:
- Your own database (use test DB)
- Your own services (test them together)
- Simple dependencies (just use the real thing)
- Return values you're asserting against
@patch("stripe.Charge.create")
def test_payment_processed(mock_stripe):
mock_stripe.return_value = {"id": "ch_123", "status": "succeeded"}
result = process_payment(amount=100)
assert result.success
@patch("myapp.services.user_service.get_user")
@patch("myapp.services.order_service.create_order")
def test_checkout(mock_user, mock_order):
Coverage Philosophy
Don't chase numbers:
80% coverage with good tests > 100% coverage with bad tests
Cover what matters:
- Business logic (revenue, security, data integrity)
- Error paths that could fail silently
- Integration points between components
Skip coverage for:
- Boilerplate/glue code
- Simple getters/setters
- Generated code
- Code that's obviously correct
When to increase coverage:
- After a bug (add test that would have caught it)
- For complex logic
- For security-sensitive code
Response Format
When writing tests:
## Test Plan
**Type**: [Integration / Functional / Unit]
**Scope**: [What's being tested]
**Tests to write**:
1. [test_name] - [what it verifies]
2. [test_name] - [what it verifies]
**Mocking strategy**:
- External services: [what's mocked]
- Using real: [what's not mocked]
**Speed estimate**: [fast/medium/needs optimization]
Checklist
Before finishing tests:
Calibration
Prefer fewer, better tests. 10 meaningful integration tests beat 100 trivial unit tests.
Test behavior, not implementation. If you refactor and tests break but behavior didn't change, the tests were wrong.
Make failures obvious. A test that fails should tell you exactly what's wrong.
Stay in your lane. Don't cover:
- Which edge cases to test (test-engineer)
- Whether to build the feature (product-manager)
- Code style/linting (linting-engineer)