Test Architect
Elite testing skill that transforms test writing into a systematic, production-grade process. Every test is written to catch real bugs and protect against regressions—not just to satisfy coverage metrics.
Core Philosophy
"A test that doesn't catch bugs is just noise in your CI pipeline."
This skill embodies the Quality Over Quantity paradigm: meaningful tests that verify behavior, not just lines covered. Every test decision is made with the understanding that:
- •Tests are documentation — They show how code should behave
- •Tests catch regressions — They prevent bugs from returning
- •Tests enable refactoring — They provide safety nets for changes
- •Tests validate edge cases — They verify behavior under unusual conditions
Test Writing Process
Test architecture involves these sequential steps:
- •Analyze Code → Understand what needs testing and its complexity
- •Identify Test Categories → Determine which test types are needed
- •Design Test Structure → Plan test organization and fixtures
- •Write Tests Systematically → Apply the testing pyramid approach
- •Validate Coverage Quality → Ensure meaningful coverage, not just line coverage
Test Categories
Determine the appropriate test types based on the code being tested:
| Code Type | Primary Tests | Reference |
|---|---|---|
| Business logic / Pure functions | Unit Tests | unit-testing.md |
| API endpoints / Routes | API Tests | api-testing.md |
| Database operations / Services | Integration Tests | integration-testing.md |
| User flows / Critical paths | E2E Tests | e2e-testing.md |
| React components / UI | Component Tests | frontend-testing.md |
| State management / Hooks | Hook Tests | frontend-testing.md |
The Testing Pyramid
Apply the testing pyramid principle for optimal coverage:
┌─────────┐
│ E2E │ ← Few, slow, verify critical paths
┌┴─────────┴┐
│Integration│ ← Some, verify component interactions
┌┴───────────┴┐
│ Unit Tests │ ← Many, fast, verify isolated logic
└─────────────┘
Test Quality Standards
The 4 Pillars of a Good Test
- •Arrangement Clarity — Setup is clear and minimal
- •Action Precision — Exactly one behavior is tested
- •Assertion Specificity — Assertions verify exact expectations
- •Isolation — Tests don't depend on other tests or shared state
Test Naming Convention
Use descriptive, behavior-focused names:
# Python - Use underscore_case with behavior description
class TestUserService:
def test_create_user_with_valid_data_returns_user_id(self):
# Follows: test_[method]_[scenario]_[expected_result]
pass
def test_create_user_with_duplicate_email_raises_conflict_error(self):
pass
// TypeScript - Use camelCase with behavior description
describe('UserService', () => {
describe('createUser', () => {
it('should return user id when data is valid', () => {
// Follows: should [expected result] when [scenario]
});
it('should throw ConflictError when email already exists', () => {
});
});
});
Pre-Testing Checklist
Before writing ANY tests, verify:
□ Do I understand WHAT the code should do (requirements)? □ Do I understand HOW the code does it (implementation)? □ Have I identified ALL logical branches? □ Have I identified edge cases (null, empty, boundary values)? □ Have I identified error conditions? □ What external dependencies need mocking? □ What is the minimum test fixture needed?
The Test Architect's 7 Commandments
1. Test Behavior, Not Implementation
- •Focus on WHAT the code does, not HOW
- •Tests should survive refactoring
- •Avoid testing private methods directly
2. One Assertion Per Concept
- •Each test verifies one logical concept
- •Multiple related assertions are OK (same concept)
- •Avoid unrelated assertions in one test
3. Test Edge Cases First
- •Null/undefined inputs
- •Empty collections/strings
- •Boundary values (0, -1, MAX_INT)
- •Invalid state combinations
4. Mock at Boundaries
- •Mock external services (APIs, databases, file system)
- •Don't over-mock (test real code)
- •Use dependency injection for testability
5. Deterministic Tests Only
- •No random values without seeding
- •No tests that depend on time
- •No tests that depend on network
- •Always use fixed test data
6. Fast Feedback Loop
- •Unit tests < 100ms each
- •Integration tests < 1s each
- •E2E tests < 10s each
- •Parallelize where possible
7. Meaningful Assertions
- •Assert specific values, not just "no error"
- •Assert data shape and content
- •Assert side effects (function calls, state changes)
- •Assert error messages, not just error types
Coverage Quality Metrics
Track these metrics beyond line coverage:
| Metric | Target | Description |
|---|---|---|
| Line Coverage | >85% | Percentage of lines executed |
| Branch Coverage | >80% | Percentage of conditional branches |
| Path Coverage | >70% | Percentage of execution paths |
| Mutation Score | >60% | Percentage of mutations killed |
Test Structure Templates
Python (Pytest)
"""Tests for [module_name].
Test coverage for:
- Happy path scenarios
- Edge cases
- Error handling
- Boundary conditions
"""
from __future__ import annotations
import pytest
from unittest.mock import Mock, patch, MagicMock
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from _pytest.fixtures import FixtureRequest
# ============================================================================
# Fixtures
# ============================================================================
@pytest.fixture
def sample_user_data() -> dict:
"""Provide minimal valid user data for testing."""
return {
"username": "testuser",
"email": "test@example.com",
"password": "SecurePass123!",
}
@pytest.fixture
def mock_database() -> Mock:
"""Provide a mock database session."""
mock_db = Mock()
mock_db.commit = Mock()
mock_db.rollback = Mock()
return mock_db
# ============================================================================
# Unit Tests
# ============================================================================
class TestUserCreation:
"""Tests for user creation functionality."""
# Happy Path Tests
def test_create_user_with_valid_data_returns_user_object(
self, sample_user_data: dict, mock_database: Mock
) -> None:
"""Verify user creation with valid data returns a User object."""
# Arrange
service = UserService(db=mock_database)
# Act
result = service.create_user(**sample_user_data)
# Assert
assert result is not None
assert result.username == sample_user_data["username"]
assert result.email == sample_user_data["email"]
mock_database.commit.assert_called_once()
# Edge Case Tests
@pytest.mark.parametrize("invalid_email", [
"", # Empty
None, # Null
"notanemail", # No @ symbol
"@nodomain", # No local part
"no@", # No domain
])
def test_create_user_with_invalid_email_raises_validation_error(
self, invalid_email: str | None, sample_user_data: dict
) -> None:
"""Verify invalid emails are rejected with appropriate error."""
# Arrange
sample_user_data["email"] = invalid_email
service = UserService(db=Mock())
# Act & Assert
with pytest.raises(ValidationError) as exc_info:
service.create_user(**sample_user_data)
assert "email" in str(exc_info.value).lower()
# Error Handling Tests
def test_create_user_rolls_back_on_database_error(
self, sample_user_data: dict, mock_database: Mock
) -> None:
"""Verify database errors trigger rollback."""
# Arrange
mock_database.commit.side_effect = DatabaseError("Connection lost")
service = UserService(db=mock_database)
# Act & Assert
with pytest.raises(DatabaseError):
service.create_user(**sample_user_data)
mock_database.rollback.assert_called_once()
# ============================================================================
# Integration Tests (with real dependencies)
# ============================================================================
class TestUserServiceIntegration:
"""Integration tests with real database."""
@pytest.fixture(autouse=True)
def setup_database(self, test_database: Database) -> None:
"""Set up clean database for each test."""
self.db = test_database
self.db.clear_all_tables()
def test_user_creation_persists_to_database(
self, sample_user_data: dict
) -> None:
"""Verify created users are persisted and retrievable."""
# Arrange
service = UserService(db=self.db)
# Act
created_user = service.create_user(**sample_user_data)
retrieved_user = service.get_user_by_id(created_user.id)
# Assert
assert retrieved_user is not None
assert retrieved_user.email == sample_user_data["email"]
TypeScript/React (Vitest)
/**
* Tests for UserService
*
* Coverage:
* - Happy path scenarios
* - Edge cases
* - Error handling
* - Async operations
*/
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { renderHook, waitFor } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { useUserProfile } from './useUserProfile';
import { api } from '../services/api';
// ============================================================================
// Mocks
// ============================================================================
vi.mock('../services/api', () => ({
api: {
get: vi.fn(),
post: vi.fn(),
},
}));
// ============================================================================
// Test Utilities
// ============================================================================
const createTestQueryClient = () =>
new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
});
const createWrapper = () => {
const queryClient = createTestQueryClient();
return ({ children }: { children: React.ReactNode }) => (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
);
};
// ============================================================================
// Fixtures
// ============================================================================
const mockUserProfile = {
id: 'user-123',
email: 'test@example.com',
displayName: 'Test User',
avatarUrl: null,
createdAt: new Date('2024-01-01'),
};
// ============================================================================
// Tests
// ============================================================================
describe('useUserProfile', () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe('Happy Path', () => {
it('should return profile data when fetch succeeds', async () => {
// Arrange
vi.mocked(api.get).mockResolvedValueOnce({ data: mockUserProfile });
// Act
const { result } = renderHook(
() => useUserProfile({ userId: 'user-123' }),
{ wrapper: createWrapper() }
);
// Assert - initial loading state
expect(result.current.isLoading).toBe(true);
expect(result.current.profile).toBeNull();
// Assert - after fetch completes
await waitFor(() => {
expect(result.current.isLoading).toBe(false);
});
expect(result.current.profile).toEqual(mockUserProfile);
expect(result.current.error).toBeNull();
});
});
describe('Edge Cases', () => {
it('should handle empty userId gracefully', async () => {
// Act
const { result } = renderHook(
() => useUserProfile({ userId: '' }),
{ wrapper: createWrapper() }
);
// Assert
await waitFor(() => {
expect(result.current.error).toBeTruthy();
});
expect(result.current.error?.message).toContain('Invalid userId');
});
it('should handle null avatarUrl in profile', async () => {
// Arrange
vi.mocked(api.get).mockResolvedValueOnce({
data: { ...mockUserProfile, avatarUrl: null },
});
// Act
const { result } = renderHook(
() => useUserProfile({ userId: 'user-123' }),
{ wrapper: createWrapper() }
);
// Assert
await waitFor(() => {
expect(result.current.profile).toBeTruthy();
});
expect(result.current.profile?.avatarUrl).toBeNull();
});
});
describe('Error Handling', () => {
it('should set error state when API call fails', async () => {
// Arrange
const apiError = new Error('Network error');
vi.mocked(api.get).mockRejectedValueOnce(apiError);
// Act
const { result } = renderHook(
() => useUserProfile({ userId: 'user-123' }),
{ wrapper: createWrapper() }
);
// Assert
await waitFor(() => {
expect(result.current.error).toBeTruthy();
});
expect(result.current.error?.message).toBe('Network error');
expect(result.current.profile).toBeNull();
});
it('should call onError callback when fetch fails', async () => {
// Arrange
const onError = vi.fn();
vi.mocked(api.get).mockRejectedValueOnce(new Error('API Error'));
// Act
renderHook(
() => useUserProfile({ userId: 'user-123', onError }),
{ wrapper: createWrapper() }
);
// Assert
await waitFor(() => {
expect(onError).toHaveBeenCalledTimes(1);
});
expect(onError).toHaveBeenCalledWith(expect.any(Error));
});
});
describe('Refresh Functionality', () => {
it('should refetch data when refresh is called', async () => {
// Arrange
vi.mocked(api.get)
.mockResolvedValueOnce({ data: mockUserProfile })
.mockResolvedValueOnce({
data: { ...mockUserProfile, displayName: 'Updated Name' },
});
// Act
const { result } = renderHook(
() => useUserProfile({ userId: 'user-123' }),
{ wrapper: createWrapper() }
);
await waitFor(() => {
expect(result.current.profile).toBeTruthy();
});
await result.current.refresh();
// Assert
await waitFor(() => {
expect(result.current.profile?.displayName).toBe('Updated Name');
});
expect(api.get).toHaveBeenCalledTimes(2);
});
});
});
Anti-Patterns to Avoid
See anti-patterns.md for comprehensive test anti-pattern detection.
Quick Reference Card
┌─────────────────────────────────────────────────────────────────────┐ │ TEST ARCHITECT QUICK REFERENCE │ ├─────────────────────────────────────────────────────────────────────┤ │ BEFORE WRITING TESTS │ │ □ Understand the code's purpose and requirements │ │ □ Identify all branches and edge cases │ │ □ Plan fixture strategy and mock boundaries │ │ □ Determine test categories (unit, integration, e2e) │ ├─────────────────────────────────────────────────────────────────────┤ │ WHILE WRITING TESTS │ │ □ Use descriptive test names (behavior-focused) │ │ □ Follow Arrange-Act-Assert pattern │ │ □ One logical concept per test │ │ □ Keep tests deterministic (no random, no time-dependent) │ │ □ Mock external dependencies only │ │ □ Test edge cases: null, empty, boundary values │ │ □ Test error scenarios explicitly │ ├─────────────────────────────────────────────────────────────────────┤ │ COVERAGE FOCUS │ │ □ Happy path (basic functionality works) │ │ □ Edge cases (unusual but valid inputs) │ │ □ Error handling (invalid inputs, failures) │ │ □ Boundary conditions (limits, transitions) │ │ □ State transitions (before/after operations) │ ├─────────────────────────────────────────────────────────────────────┤ │ TEST QUALITY │ │ □ Tests are fast (<100ms for unit tests) │ │ □ Tests are isolated (no shared state) │ │ □ Tests are repeatable (same result every time) │ │ □ Tests are self-validating (pass/fail is clear) │ │ □ Tests are timely (written with the code) │ ├─────────────────────────────────────────────────────────────────────┤ │ FINAL CHECK │ │ □ Would this test catch a regression? │ │ □ Would this test survive a refactoring? │ │ □ Is this testing behavior or implementation? │ │ □ Would I trust this test to protect production? │ └─────────────────────────────────────────────────────────────────────┘
Activation Protocol
When Test Architect mode is activated:
- •Analyze the target code — Understand what needs testing
- •Identify coverage gaps — Find untested paths and edge cases
- •Choose test types — Unit, integration, e2e as appropriate
- •Design minimal fixtures — Only what's needed for the tests
- •Write tests systematically — Happy path → edge cases → errors
- •Validate coverage quality — Branch coverage, not just line coverage
Remember: The goal isn't 100% coverage—it's meaningful tests that catch bugs and prevent regressions. Tests that your future self (and teammates) will trust.