ARC // OS — Test Quality Standards
Comprehensive guide for writing high-quality tests that are independent, maintainable, and reliable.
Table of Contents
- Every Test Must
- Test Structure
- Mocking Strategy
- Quality Checklist
- Examples: Good vs Bad
- Common Anti-Patterns
Every Test Must
1. Be Independent
Requirement: No shared state between tests. Each test must be able to run in isolation.
✅ Good:
describe('TaskService', () => {
beforeEach(async () => {
await cleanupTestUsers(); // Clean state before each test
});
it('should create task', async () => {
const service = new TaskService();
const user = await createTestUser('test@example.com');
const result = await service.createTask(user.id, { title: 'Test', timebox: 30 });
expect(result).toBeDefined();
});
it('should list tasks', async () => {
const service = new TaskService();
const user = await createTestUser('test2@example.com'); // New user for this test
const tasks = await service.listTasks(user.id);
expect(tasks).toEqual([]);
});
});
❌ Bad:
describe('TaskService', () => {
let sharedUser: User; // Shared state
beforeAll(async () => {
sharedUser = await createTestUser('test@example.com');
});
it('should create task', async () => {
// Uses sharedUser - test depends on previous test
const result = await service.createTask(sharedUser.id, { title: 'Test', timebox: 30 });
expect(result).toBeDefined();
});
it('should list tasks', async () => {
// Assumes task was created in previous test - NOT INDEPENDENT
const tasks = await service.listTasks(sharedUser.id);
expect(tasks.length).toBe(1); // Fails if run alone
});
});
2. Clean Up After Itself
Requirement: Tests must clean up any data they create or modify.
✅ Good:
describe('TaskService', () => {
beforeEach(async () => {
await cleanupTestUsers(); // Clean before each test
});
afterEach(async () => {
await cleanupTestUsers(); // Clean after each test (defensive)
});
it('should create task', async () => {
const user = await createTestUser('test@example.com');
// Test creates data, but cleanupTestUsers() will clean it
});
});
❌ Bad:
describe('TaskService', () => {
it('should create task', async () => {
const user = await createTestUser('test@example.com');
// No cleanup - data persists and can affect other tests
});
});
3. Have Clear Name
Requirement: Test name clearly describes what it tests.
✅ Good:
should create task when valid input providedshould return error when user not authenticatedshould update task status to done when completedshould require nextStep when deferring task
❌ Bad:
test1workstest createshould worktest task
4. Test One Thing
Requirement: Each test should verify one specific behavior or outcome.
✅ Good:
it('should create task with required fields', async () => {
const result = await service.createTask(userId, { title: 'Test', timebox: 30 });
expect(result.title).toBe('Test');
});
it('should set default status to todo', async () => {
const result = await service.createTask(userId, { title: 'Test', timebox: 30 });
expect(result.status).toBe('todo');
});
it('should require timebox', async () => {
await expect(
service.createTask(userId, { title: 'Test' }) // Missing timebox
).rejects.toThrow();
});
❌ Bad:
it('should create task correctly', async () => {
const result = await service.createTask(userId, { title: 'Test', timebox: 30 });
expect(result.title).toBe('Test');
expect(result.status).toBe('todo');
expect(result.timebox).toBe(30);
expect(result.userId).toBe(userId);
expect(result.createdAt).toBeDefined();
// Testing too many things - if one fails, unclear which assertion failed
});
5. Be Deterministic
Requirement: Same input = same output, always. No randomness, no time-dependent behavior.
✅ Good:
it('should create task', async () => {
const input = { title: 'Test Task', timebox: 30 };
const result1 = await service.createTask(userId, input);
const result2 = await service.createTask(userId, input);
// Same input produces consistent results
expect(result1.title).toBe(result2.title);
});
❌ Bad:
it('should create task', async () => {
const input = { title: `Test ${Math.random()}`, timebox: 30 }; // Random!
const result = await service.createTask(userId, input);
expect(result).toBeDefined();
// Different result every time - not deterministic
});
For Time-Dependent Tests:
// ✅ Good: Use fixed dates
const fixedDate = new Date('2026-01-15');
const day = await createTestDay(weekId, fixedDate);
// ❌ Bad: Use current date
const day = await createTestDay(weekId, new Date()); // Changes every day
6. Run Fast
Requirement: Unit tests < 100ms, integration tests < 1s, total suite < 2 minutes.
✅ Good:
it('should create task', async () => {
const result = await service.createTask(userId, { title: 'Test', timebox: 30 });
expect(result).toBeDefined();
// Fast: Direct service call, no network, no delays
});
❌ Bad:
it('should create task', async () => {
await new Promise(resolve => setTimeout(resolve, 1000)); // Unnecessary delay
const result = await service.createTask(userId, { title: 'Test', timebox: 30 });
expect(result).toBeDefined();
});
Timeout Configuration:
- Test timeout: 10 seconds max (configured in vitest.config.ts)
waitFortimeout: 5 seconds max- No infinite waits
Test Structure
Arrange-Act-Assert Pattern
Every test should follow this structure:
it('should do X when Y', async () => {
// Arrange: Set up test data
const service = new TaskService();
const user = await createTestUser('test@example.com');
const input = { title: 'Test Task', timebox: 30 };
// Act: Call the method being tested
const result = await service.createTask(user.id, input);
// Assert: Verify the result
expect(result).toBeDefined();
expect(result.title).toBe('Test Task');
expect(result.status).toBe('todo');
});
Clear Setup/Teardown
✅ Good:
describe('TaskService', () => {
let userId: string;
let service: TaskService;
beforeEach(async () => {
// Setup: Clean state and create test data
await cleanupTestUsers();
const user = await createTestUser('test@example.com');
userId = user.id;
service = new TaskService();
});
afterEach(async () => {
// Teardown: Clean up (defensive)
await cleanupTestUsers();
});
describe('createTask', () => {
it('should create task', async () => {
// Test uses userId and service from beforeEach
const result = await service.createTask(userId, { title: 'Test', timebox: 30 });
expect(result).toBeDefined();
});
});
});
❌ Bad:
describe('TaskService', () => {
// No setup/teardown - tests may interfere with each other
it('should create task', async () => {
const service = new TaskService();
const user = await createTestUser('test@example.com');
// No cleanup - data persists
});
});
Meaningful Assertions
✅ Good:
it('should create task with correct data', async () => {
const result = await service.createTask(userId, { title: 'My Task', timebox: 60 });
expect(result.title).toBe('My Task'); // Specific assertion
expect(result.timebox).toBe(60); // Specific assertion
expect(result.status).toBe('todo'); // Specific assertion
expect(result.userId).toBe(userId); // Specific assertion
});
❌ Bad:
it('should create task', async () => {
const result = await service.createTask(userId, { title: 'My Task', timebox: 60 });
expect(result).toBeDefined(); // Too vague - what exactly are we testing?
expect(result).toBeTruthy(); // Even more vague
});
Frontend Example:
// ✅ Good: Specific assertions
it('should render button with text', () => {
render(<Button>Click me</Button>);
expect(screen.getByText('Click me')).toBeDefined();
expect(screen.getByRole('button')).toBeDefined();
});
// ❌ Bad: Vague assertions
it('should render button', () => {
render(<Button>Click me</Button>);
expect(screen.getByRole('button')).toBeDefined(); // What about the text?
});
Mocking Strategy
What to Mock
✅ Mock External Dependencies
External APIs:
// ✅ Good: Mock external API
vi.mock('../../utils/api', () => ({
api: {
get: vi.fn(),
post: vi.fn(),
},
}));
// In test
vi.mocked(api.get).mockResolvedValue({
ok: true,
data: mockData,
});
Email Service:
// ✅ Good: Email service is automatically mocked
// Access mock to verify emails sent
import { getMockEmailService } from '../services/email.service';
const mockService = getMockEmailService();
if (mockService) {
const emails = mockService.getSentEmails();
expect(emails.length).toBeGreaterThan(0);
}
JWT (Unit Tests Only):
// ✅ Good: Mock JWT for unit tests
vi.mock('jsonwebtoken', () => ({
default: {
sign: vi.fn().mockReturnValue('mock-token'),
verify: vi.fn().mockReturnValue({ id: 'user-123', email: 'test@example.com' }),
},
}));
What NOT to Mock
❌ Don't Mock What You're Testing
Database (Integration Tests):
// ✅ Good: Use real database for integration tests
describe('POST /api/tasks', () => {
it('should create task', async () => {
const response = await app.inject({
method: 'POST',
url: '/api/tasks',
payload: { title: 'Test', timebox: 30 },
headers: { cookie: `token=${authToken}` },
});
// Real database, real queries, real constraints
});
});
Services (Unit Tests):
// ✅ Good: Test service with real database
describe('TaskService', () => {
it('should create task', async () => {
const service = new TaskService(); // Real service
const result = await service.createTask(userId, { title: 'Test', timebox: 30 });
// Real database, real business logic
});
});
// ❌ Bad: Don't mock the service you're testing
vi.mock('../services/task.service'); // Can't test it if it's mocked!
const service = new TaskService(); // This is mocked - useless
Mocking Patterns
Pattern 1: Mock External API (Frontend)
vi.mock('../../utils/api');
beforeEach(() => {
vi.clearAllMocks(); // Clean mocks between tests
});
it('should load data', async () => {
vi.mocked(api.get).mockResolvedValue({
ok: true,
data: mockData,
});
render(<Page />);
await waitFor(() => {
expect(screen.getByText('Expected')).toBeDefined();
}, { timeout: 5000 });
});
Pattern 2: Use Real Database (Backend)
describe('TaskService', () => {
beforeEach(async () => {
await cleanupTestUsers(); // Clean real database
});
it('should create task', async () => {
const service = new TaskService();
// Uses real Prisma, real database, real constraints
const result = await service.createTask(userId, input);
expect(result).toBeDefined();
});
});
Quality Checklist
Before submitting a test, verify:
- Independence: Test can run alone without other tests
- Cleanup: Test cleans up any data it creates
- Clear Name: Test name describes what it tests
- One Thing: Test verifies one specific behavior
- Deterministic: Same input = same output
- Fast: Test completes quickly (< 1s for integration, < 100ms for unit)
- Structure: Follows Arrange-Act-Assert pattern
- Setup/Teardown: Proper beforeEach/afterEach hooks
- Assertions: Specific, meaningful assertions
- Mocking: Only mocks external dependencies, not what's being tested
- Database: Uses real database for integration tests
- Timeouts: All
waitForcalls have explicit timeouts (max 5s)
Examples: Good vs Bad
Example 1: Test Independence
✅ Good:
describe('TaskService', () => {
beforeEach(async () => {
await cleanupTestUsers();
});
it('should create task', async () => {
const user = await createTestUser('test1@example.com');
const service = new TaskService();
const result = await service.createTask(user.id, { title: 'Test', timebox: 30 });
expect(result).toBeDefined();
});
it('should list tasks', async () => {
const user = await createTestUser('test2@example.com'); // Different user
const service = new TaskService();
const tasks = await service.listTasks(user.id);
expect(tasks).toEqual([]); // Independent - doesn't depend on previous test
});
});
❌ Bad:
describe('TaskService', () => {
let sharedUser: User;
beforeAll(async () => {
sharedUser = await createTestUser('test@example.com');
});
it('should create task', async () => {
await service.createTask(sharedUser.id, { title: 'Test', timebox: 30 });
});
it('should list tasks', async () => {
const tasks = await service.listTasks(sharedUser.id);
expect(tasks.length).toBe(1); // Depends on previous test - NOT INDEPENDENT
});
});
Example 2: Clear Test Names
✅ Good:
it('should return error when task not found', async () => {
await expect(service.getTask('non-existent-id', userId)).rejects.toThrow('Task not found');
});
it('should require nextStep when deferring task', async () => {
await expect(
service.updateTask(taskId, userId, { status: 'deferred' })
).rejects.toThrow('nextStep is required');
});
❌ Bad:
it('test1', async () => {
// What does this test?
});
it('works', async () => {
// What works?
});
it('should work', async () => {
// What should work?
});
Example 3: One Thing Per Test
✅ Good:
it('should create task with title', async () => {
const result = await service.createTask(userId, { title: 'My Task', timebox: 30 });
expect(result.title).toBe('My Task');
});
it('should set default status to todo', async () => {
const result = await service.createTask(userId, { title: 'My Task', timebox: 30 });
expect(result.status).toBe('todo');
});
❌ Bad:
it('should create task correctly', async () => {
const result = await service.createTask(userId, { title: 'My Task', timebox: 30 });
expect(result.title).toBe('My Task');
expect(result.status).toBe('todo');
expect(result.timebox).toBe(30);
expect(result.userId).toBe(userId);
expect(result.createdAt).toBeDefined();
// Testing too many things - unclear which assertion failed if test fails
});
Example 4: Proper Mocking
✅ Good:
// Frontend: Mock API
vi.mock('../../utils/api', () => ({
api: { get: vi.fn(), post: vi.fn() },
}));
it('should load data', async () => {
vi.mocked(api.get).mockResolvedValue({ ok: true, data: mockData });
render(<Page />);
// Test component behavior, not API implementation
});
❌ Bad:
// Backend: Don't mock the service you're testing
vi.mock('../services/task.service'); // ❌ Can't test it if it's mocked!
const service = new TaskService(); // This is mocked - useless
Common Anti-Patterns
❌ Shared State Between Tests
// ❌ BAD: Shared state
let sharedData: unknown;
beforeAll(async () => {
sharedData = await createData();
});
it('test1', () => {
// Uses sharedData
});
it('test2', () => {
// Also uses sharedData - tests are coupled
});
Fix: Use beforeEach to create fresh data for each test.
❌ No Cleanup
// ❌ BAD: No cleanup
it('should create task', async () => {
await service.createTask(userId, input);
// Data persists and can affect other tests
});
Fix: Always use cleanupTestUsers() in beforeEach or afterEach.
❌ Vague Test Names
// ❌ BAD: Vague names
it('test1', () => {});
it('works', () => {});
it('should work', () => {});
Fix: Use descriptive names: should do X when Y.
❌ Testing Multiple Things
// ❌ BAD: Testing multiple behaviors
it('should create task correctly', async () => {
expect(result.title).toBe('Test');
expect(result.status).toBe('todo');
expect(result.timebox).toBe(30);
expect(result.userId).toBe(userId);
// Too many assertions - unclear what's being tested
});
Fix: Split into multiple tests, each testing one thing.
❌ Non-Deterministic Tests
// ❌ BAD: Random or time-dependent
it('should create task', async () => {
const input = { title: `Test ${Math.random()}`, timebox: 30 };
// Different every time
});
it('should create task', async () => {
const day = await createTestDay(weekId, new Date()); // Changes every day
});
Fix: Use fixed values and fixed dates.
❌ Mocking What You're Testing
// ❌ BAD: Mocking the service being tested
vi.mock('../services/task.service');
const service = new TaskService(); // Mocked - can't test it
Fix: Only mock external dependencies, not what you're testing.
❌ Missing Timeouts
// ❌ BAD: No timeout - can hang forever
await waitFor(() => {
expect(screen.getByText('Expected')).toBeDefined();
});
Fix: Always specify timeout: waitFor(() => {...}, { timeout: 5000 }).
Summary
Every test must:
- ✅ Be independent (no shared state)
- ✅ Clean up after itself
- ✅ Have clear name
- ✅ Test one thing
- ✅ Be deterministic
- ✅ Run fast
Test structure:
- ✅ Arrange-Act-Assert pattern
- ✅ Clear setup/teardown
- ✅ Meaningful assertions
Mocking:
- ✅ Mock external dependencies
- ❌ Don't mock what you're testing
- ✅ Use real DB for integration tests
Last Updated: 2026-01-13