Unit Testing Skill
Fast, isolated tests that verify individual functions and modules. Unit tests run in the CI runner (not Kubernetes) for rapid feedback.
Fast, isolated tests that verify individual functions and modules. Unit tests run in the CI runner (not Kubernetes) for rapid feedback.
--- name: unit-testing description: Unit testing patterns with Vitest - mocking, fixtures, isolation, and fast feedback. user-invocable: false ---
Fast, isolated tests that verify individual functions and modules. Unit tests run in the CI runner (not Kubernetes) for rapid feedback.
---
| Aspect | Details | |--------|---------| | Location | `components/*/src/**/*.test.ts` | | Framework | Vitest | | Runs In | CI runner (fast feedback) | | Written By | Implementors (alongside their code) |
---
Place test files next to the code they test:
src/model/use-cases/
├── create_user.ts
├── create_user.test.ts # Unit test for create_user
├── update_user.ts
└── update_user.test.ts// src/model/use-cases/create_user.test.ts
import { describe, it, expect } from 'vitest';
import { createUser } from './create_user';
import type { Dependencies } from '../dependencies';
/**
* @spec changes/user-management/SPEC.md
* @issue PROJ-123
*/
describe('createUser', () => {
describe('AC1: Valid user creation', () => {
it('creates user when email is unique', async () => {
// Arrange (Given)
const mockDeps = createMockDependencies();
const args = { email: 'test@example.com', name: 'Test User' };
// Act (When)
const result = await createUser(mockDeps, args);
// Assert (Then)
expect(result.success).toBe(true);
if (result.success) {
expect(result.user.email).toBe('test@example.com');
}
});
});
});---
For functions that receive dependencies as the first argument (CMDO pattern):
import type { Dependencies } from '../dependencies';
const createMockDependencies = (overrides?: Partial<Dependencies>): Dependencies => ({
findUserByEmail: async () => null,
findUserById: async () => null,
insertUser: async (data) => ({ id: 'mock-id', ...data, createdAt: new Date() }),
updateUser: async (id, data) => ({ id, ...data, updatedAt: new Date() }),
...overrides,
});
// Usage in tests
it('returns error when email exists', async () => {
const deps = createMockDependencies({
findUserByEmail: async () => ({ id: '123', email: 'exists@test.com', name: 'Existing' }),
});
const result = await createUser(deps, { email: 'exists@test.com', name: 'New' });
expect(result.success).toBe(false);
});For tracking calls and controlling return values:
import { describe, it, expect, vi } from 'vitest';
it('calls insertUser with correct data', async () => {
const insertUser = vi.fn().mockResolvedValue({ id: '123', email: 'test@example.com' });
const deps = createMockDependencies({ insertUser });
await createUser(deps, { email: 'test@example.com', name: 'Test' });
expect(insertUser).toHaveBeenCalledOnce();
expect(insertUser).toHaveBeenCalledWith(
expect.objectContaining({ email: 'test@example.com' })
);
});For mocking entire modules (use sparingly):
import { vi } from 'vitest';
vi.mock('../services/email', () => ({
sendEmail: vi.fn().mockResolvedValue({ sent: true }),
}));
import { sendEmail } from '../services/email';
it('sends welcome email after user creation', async () => {
await createUserWithWelcomeEmail(deps, { email: 'new@test.com' });
expect(sendEmail).toHaveBeenCalledWith(
expect.objectContaining({ to: 'new@test.com', template: 'welcome' })
);
});---
Create reusable factories for test data:
// src/__tests__/fixtures/user.ts
import type { User } from '../../types/generated';
export const createTestUser = (overrides?: Partial<User>): User => ({
id: 'test-user-id',
email: 'test@example.com',
name: 'Test User',
role: 'planner',
clientId: 'test-client',
createdAt: new Date('2026-01-01'),
updatedAt: new Date('2026-01-01'),
...overrides,
});
// Usage
it('formats user display name', () => {
const user = createTestUser({ name: 'John Doe' });
expect(formatDisplayName(user)).toBe('John Doe');
});
it('uses email when name is empty', () => {
const user = createTestUser({ name: '', email: 'john@example.com' });
expect(formatDisplayName(user)).toBe('john');
});// src/__tests__/fixtures/constants.ts
export const TEST_USER_ID = 'test-user-123';
export const TEST_CLIENT_ID = 'test-client-456';
export const TEST_EMAIL = 'test@example.com';
export const VALID_CREDENTIALS = {
email: TEST_EMAIL,
password: 'ValidPass123!',
};
export const INVALID_CREDENTIALS = {
email: TEST_EMAIL,
password: 'wrong',
};---
import { describe, it, beforeEach, afterEach, vi } from 'vitest';
describe('userService', () => {
beforeEach(() => {
vi.clearAllMocks();
});
afterEach(() => {
vi.restoreAllMocks();
});
// Tests run in isolation
});// BAD: Shared mutable state
let testUser = { id: '1', name: 'Test' };
it('test 1', () => {
testUser.name = 'Modified'; // Affects other tests!
});
// GOOD: Create fresh data per test
it('test 1', () => {
const testUser = createTestUser();
// Modifications don't affect other tests
});import { vi, beforeEach, afterEach } from 'vitest';
describe('time-dependent tests', () => {
beforeEach(() => {
vi.useFakeTimers();
vi.setSystemTime(new Date('2026-01-15T10:00:00Z'));
});
afterEach(() => {
vi.useRealTimers();
});
it('calculates expiration correctly', () => {
const token = createToken({ expiresIn: '1h' });
expect(token.expiresAt).toEqual(new Date('2026-01-15T11:00:00Z'));
});
});---
it('handles async operations', async () => {
const result = await createUser(deps, validArgs);
expect(result.success).toBe(true);
});
it('handles rejected promises', async () => {
const deps = createMockDependencies({
insertUser: async () => { throw new Error('DB connection failed'); },
});
await expect(createUser(deps, validArgs)).rejects.toThrow('DB connection failed');
});it('returns error result for validation failure', async () => {
const result = await createUser(deps, { email: 'invalid-email', name: '' });
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error).toBe('invalid_email');
}
});
it('handles database errors gracefully', async () => {
const deps = createMockDependencies({
insertUser: async () => { throw new Error('Connection timeout'); },
});
const result = await createUser(deps, validArgs);
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error).toBe('database_error');
}
});---
For functions returning discriminated union results:
type CreateUserResult =
| { readonly success: true; readonly user: User }
| { readonly success: false; readonly error: 'email_exists' | 'invalid_email' };
it('returns success with user data', async () => {
const result = await createUser(deps, validArgs);
// Type narrowing via discriminant
expect(result.success).toBe(true);
if (result.success) {
expect(result.user.email).toBe('test@example.com');
expect(result.user.id).toBeDefined();
}
});
it('returns specific error code', async () => {
const deps = createMockDependencies({
findUserByEmail: async () => existingUser,
});
const result = await createUser(deps, { email: existingUser.email, name: 'New' });
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error).toBe('email_exists');
}
});---
| Metric | Target | |--------|--------| | Line coverage | ≥80% | | Branch coverage | ≥75% | | Function coverage | ≥90% |
---
---
Before committing unit tests, verify:
---
This skill defines no input parameters or structured output.