skills/testing-unit-integration/SKILL.md
Expert guidance for writing clean, simple, and effective unit, integration, component, microservice, and API tests. Use this skill when reviewing existing tests for violations, writing new tests, or refactoring tests. NOT for end-to-end tests that span multiple processes - use testing-e2e skill instead. Covers AAA pattern, data factories, mocking strategies, DOM testing, database testing, and assertion best practices.
npx skillsauth add agdev/claude-code testing-unit-integrationInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Expert guidance for keeping tests simple, clean, consistent, and short. When reviewing tests, report violated rule numbers.
Scope: Unit, integration, component, microservice, API tests. NOT for e2e tests spanning multiple processes - use testing-e2e skill instead.
These are absolutely critical - stop coding if you can't follow them:
Each data point in assertion must appear in arrange phase - shows cause and effect clearly.
// Arrange
const activeOrder = buildOrder({ status: 'active' })
// Assert - references arranged data directly
expect(result.id).toBe(activeOrder.id) // ✅ Clear connection
expect(result.id).toBe('123') // ❌ Magic value
Anything affecting test directly should exist in the test. Implicit effects go in beforeEach, never in external files.
Cover a little more than needed. Testing save? Use two items. Testing filter? Also verify items that should NOT appear.
Choose options more likely to fail. Picking user role? Use least privileged one.
When {scenario}, then {expectation}activeOrder.id not '123')any. Use obj as unknown as Type for invalid inputs/test/helpers folderimport { faker } from "@faker-js/faker";
import { Order } from "../types";
export function buildOrder(overrides: Partial<Order> = {}): Order {
return {
id: faker.string.uuid(),
customerName: faker.person.fullName(),
status: faker.helpers.arrayElement(["active", "completed", "cancelled"]),
items: [buildOrderItem(), buildOrderItem()], // Default 2 items
...overrides,
};
}
// ❌ WEAK - Multiple redundant assertions
expect(response).not.toBeNull()
expect(Array.isArray(response)).toBe(true)
expect(response.length).toBe(2)
expect(response[0].id).toBe('123')
// ✅ STRONG - Single assertion catches all issues
expect(response).toEqual([{id: '123'}, {id: '456'}])
Cloud/External SDK mocking: See references/aws-sdk-mocking.md for AWS SDK patterns (also applicable to other cloud SDKs).
For React Testing Library, Playwright component tests, Storybook:
// ❌ BAD - Only checks element exists, not its state
expect(screen.getByText('App Name')).toBeInTheDocument();
// ✅ GOOD - Verifies actual user-visible state
expect(screen.getByRole('checkbox', { name: /app name/i })).toBeChecked();
expect(within(row).getByText(/associated/i)).toBeInTheDocument();
expect(screen.getByRole('button', { name: /submit/i })).toBeDisabled();
expect.any(Number)${faker.internet.email()}-${faker.string.nanoid(5)}For testing debounce, throttle, cache TTL, polling, setTimeout/setInterval logic.
Detailed guide: See references/fake-timers.md for patterns and anti-patterns.
beforeEach(() => vi.useFakeTimers()), afterEach(() => vi.useRealTimers())runAllTimersAsync() when timer count is unknownrejects matcher OR real timers with short delays// GOOD: Start promise, advance time, then await
const promise = functionWithTimeout();
await vi.advanceTimersByTimeAsync(1000);
const result = await promise;
// BAD: Await immediately (hangs forever - timers frozen)
const result = await functionWithTimeout(); // ❌ Never completes!
// For rejected promise tests - use rejects matcher
await expect(handler.execute(mockFn)).rejects.toThrow('fail');
// OR use real timers with short delays
vi.useRealTimers();
const config = { maxRetries: 2, delay: 1 }; // 1ms delay
await expect(retryWithBackoff(mockFn, config)).rejects.toThrow('fail');
When testing frontend-backend integration, validate contracts between layers.
as never, as any, as unknown on mock return values - defeats TypeScript safety// ❌ BAD - Type escape hatch hides contract mismatch
vi.spyOn(api, 'create').mockResolvedValue(mockData as never);
// ❌ BAD - Frontend mock doesn't match actual backend
vi.mocked(api.create).mockResolvedValue({
application: data, // Frontend WANTS this
});
// But backend ACTUALLY returns: { data: {...} }
// ✅ GOOD - Mock matches actual backend response
const mockResponse: CreateResponse = {
data: mockApp, // Match ACTUAL backend
created: true,
};
vi.spyOn(api, 'create').mockResolvedValue(mockResponse);
// ✅ GOOD - Destructure as consumers will (catches mismatches)
const { application } = response; // Will fail if backend uses 'data' not 'application'
Mock data must reflect reality, not fantasy.
/**
* Real API response from: GET /api/users/123/applications
* Captured: 2025-12-04
* Backend version: [email protected]
*
* Update this fixture if backend changes response structure.
*/
export const REAL_USER_APP_MAPPING: UserAppMapping = {
_id: '', // Mapping ID (often empty)
applicationId: 'app-123', // CRITICAL: This is the app ID!
isActiveForApp: true,
application: { /* ... */ }
};
// Edge case fixtures
export const EDGE_CASES = {
emptyList: [],
nullField: { ...REAL_USER_APP_MAPPING, applicationId: null },
missingOptional: { _id: '', applicationId: 'app-1' }, // No 'application' field
};
Boolean flags in API responses control critical behavior - test both states.
@warning JSDoc to helpers with dangerous defaults that could mask bugs// ❌ BAD - Only tests one state (default true)
const mappings = [
createMapping({ applicationId: 'app1' }) // isActiveForApp defaults to true
];
// ✅ GOOD - Tests both states explicitly
const mappings = [
createMapping({ applicationId: 'app1', isActiveForApp: true }), // Active
createMapping({ applicationId: 'app2', isActiveForApp: false }), // Inactive
];
// ✅ GOOD - Document dangerous defaults
/**
* @warning The default `isActiveForApp` is TRUE. When testing inactive
* associations, you MUST explicitly set `isActiveForApp: false`.
* Forgetting this will cause tests to show associations as active
* when they should be inactive.
*/
export function createMapping(overrides: Partial<Mapping> = {}): Mapping {
return {
isActiveForApp: true, // Dangerous default - document it!
...overrides,
};
}
Test error scenarios with correct HTTP semantics - 404 is NOT 503.
rejects.toThrow() needs error type, not just any errorDetailed guide: See references/error-handling-matrix.md for HTTP status mapping and test patterns.
// ❌ BAD - Wrong error mapping (404 is NOT service unavailable)
it('When user not found, then throws ServiceUnavailableError', async () => {
nock(API_URL).get('/user/123').reply(404);
await expect(service.getUser('123')).rejects.toThrow(ServiceUnavailableError);
});
// ❌ BAD - Generic assertion (any error passes)
await expect(service.getUser('123')).rejects.toThrow();
// ✅ GOOD - Correct error type for HTTP 404
it('When user not found (404), then throws NotFoundError', async () => {
nock(API_URL).get('/user/123').reply(404, { message: 'User not found' });
await expect(service.getUser('123')).rejects.toThrow(NotFoundError);
await expect(service.getUser('123')).rejects.toThrow(/user.*not found/i);
});
// ✅ GOOD - Test both error type AND message context
it('When unauthorized (401), then throws UnauthorizedError with context', async () => {
nock(API_URL).get('/user/123').reply(401);
await expect(service.getUser('123')).rejects.toThrow(UnauthorizedError);
});
Achieve comprehensive coverage efficiently:
it('should test orders filtering', async () => { // ❌ A.1 - vague title
const adminUser = { role: 'admin' } // ❌ I.10 - use least privilege
const mockOrderService = vi.fn() // ❌ E.1 - mocking internal
const testData = [{ id: 1, name: 'test1' }] // ❌ C.10 - meaningless data
render(<OrdersReport data={testData} />)
const component = screen.getByTestId('orders-report') // ❌ F.1 - test-id
try { // ❌ A.13 - try-catch not allowed
await userEvent.click(screen.getByRole('button'))
let found = [] // ❌ D.7 - custom coding
for (const row of rows) { found.push(row) } // ❌ A.13 - loop
expect(found.length).toBe(5) // ❌ B.3 - data not in arrange
expect(mockOrderService).toHaveBeenCalled() // ❌ B.23 - implementation detail
} catch (error) {
console.log('Failed:', error) // ❌ A.13 - console.log
}
})
beforeEach(() => {
const currentUser = buildUser({ role: 'viewer' }) // Deliberate fire
http.get('/api/user/1', () => HttpResponse.json(currentUser))
})
test('When filtering by active status, then only active orders displayed', async () => {
// Arrange
const activeOrder = buildOrder({ customerName: faker.person.fullName(), status: 'active' })
const completedOrder = buildOrder({ customerName: faker.person.fullName(), status: 'completed' })
http.get('/api/orders', () => HttpResponse.json([activeOrder, completedOrder]))
const screen = render(<OrdersReport />)
// Act
await userEvent.click(screen.getByRole('button', { name: 'Filter by Active' }))
// Assert
expect.element(screen.getByRole('cell', { name: activeOrder.customerName })).toBeVisible()
expect.element(screen.getByRole('cell', { name: completedOrder.customerName })).not.toBeVisible() // Extra mile
})
When reviewing tests, report violations as:
Line X: Violates [RULE_NUMBER] - [Brief explanation]
Example:
Line 15: Violates A.13 - Contains try-catch block, tests must be flat
Line 23: Violates B.3 - Assertion uses '123' but this value not in Arrange phase
Line 31: Violates F.1 - Uses getByTestId, should use getByRole or getByLabel
tools
Expert guidance for writing resilient end-to-end tests that span multiple processes and components. Use this skill when reviewing, writing, or refactoring system-wide e2e tests. Covers user-facing scenarios, helper functions, data factories, ARIA-based selectors, and auto-retriable assertions. NOT for unit/integration tests - use testing-unit-integration skill instead. See references/playwright-resilience.md for detailed selector patterns.
tools
Guide for creating effective skills. This skill should be used when users want to create a new skill (or update an existing skill) that extends Claude's capabilities with specialized knowledge, workflows, or tool integrations.
data-ai
Expert guidance for creating, updating, and maintaining CLAUDE.md memory files following best practices for hierarchical memory, structure, content organization, and team collaboration
tools
Expert guidance for creating effective Claude Code agents (subagents). Use when users want to create a new agent, update an existing agent, or learn agent design best practices. Covers agent architecture, prompt engineering, tool selection, model choice, and common pitfalls. Integrates with skill-creator when agent needs accompanying skills.