Skip to main content

ARC // OS — Test Quality Standards

Comprehensive guide for writing high-quality tests that are independent, maintainable, and reliable.

Table of Contents​

  1. Every Test Must
  2. Test Structure
  3. Mocking Strategy
  4. Quality Checklist
  5. Examples: Good vs Bad
  6. 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 provided
  • should return error when user not authenticated
  • should update task status to done when completed
  • should require nextStep when deferring task

❌ Bad:

  • test1
  • works
  • test create
  • should work
  • test 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)
  • waitFor timeout: 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 waitFor calls 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:

  1. ✅ Be independent (no shared state)
  2. ✅ Clean up after itself
  3. ✅ Have clear name
  4. ✅ Test one thing
  5. ✅ Be deterministic
  6. ✅ 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