Write Unit Tests
Add focused tests for changed behavior, bug seams, boundary cases, and regression risk using the project's existing test conventions.
Usage
Typical invocations:
- Claude Code:
/v1-write-tests [target] - Codex: invoke
v1-write-testsfrom the skills menu or use$v1-write-tests [target]
Target (optional):
- If not specified: tests cover the diff between current branch and main
- If specified: tests cover the specified file or module
Examples:
/v1-write-tests # Test all changes vs main
/v1-write-tests src/core/query.py # Test specific file
In Codex, the slash examples below map directly to $v1-write-tests ....
What It Does
1. Test Selection
- Identify the changed public behavior from the diff, target file, or user request.
- For bug fixes, reproduce the real failure pattern at the narrowest correct seam before asserting the fix.
- Cover boundary and error paths that the changed behavior can realistically hit.
- Add a test only when it can fail for a meaningful regression; skip coverage that only executes lines.
- For each changed branch or state transition, prefer at least one assertion that would fail if that branch were removed, inverted, or bypassed.
2. Test Structure
- Uses project's testing framework conventions
- Names each test after the behavior and condition under test
- Follows Arrange-Act-Assert pattern
- Groups related tests logically
3. Test Cases Include
- Happy path scenarios
- Edge cases and boundary conditions
- Error handling and exception cases
- Appropriate mocking of external dependencies
4. Test Quality
- Tests are independent and isolated
- Tests are deterministic and repeatable
- Each test has one primary reason to fail
- Assertion messages or comments explain non-obvious invariants only
- External APIs, wall-clock time, network calls, and shared mutable state are isolated behind fixtures, fakes, or test doubles unless the project explicitly uses integration tests for that layer
Style
- Arrange-Act-Assert pattern
- Descriptive test names (test_method_condition_expected_result)
- Test user-visible UI text when it is the behavior; otherwise prefer roles, labels, states, and user outcomes over brittle copy assertions
- Isolated and deterministic
What NOT to Do (Anti-Patterns)
Three rules, each explained below: assert on real behavior, not mocks; keep test-only code out of production classes; understand a dependency before mocking it.
Anti-Pattern 1: Testing Mock Behavior
Bad:
// Testing that mock exists, not real behavior
test('renders sidebar', () => {
render(<Page />);
expect(screen.getByTestId('sidebar-mock')).toBeInTheDocument();
});
Fix: Test real component or don't mock it. If you must mock, don't assert on mock elements.
Anti-Pattern 2: Test-Only Methods in Production
Bad: Adding destroy() to a class only because tests need cleanup.
// BAD: destroy() only used in tests
class Session {
async destroy() { /* cleanup */ }
}
// In tests
afterEach(() => session.destroy());
Fix: Put cleanup utilities in test-utils/, not production code.
// GOOD: Test utility
// test-utils/cleanup.ts
export async function cleanupSession(session: Session) {
const workspace = session.getWorkspaceInfo();
if (workspace) await workspaceManager.destroyWorkspace(workspace.id);
}
// In tests
afterEach(() => cleanupSession(session));
Anti-Pattern 3: Mocking Without Understanding
Bad: Mocking "to be safe" without understanding what the test depends on.
// Mock breaks test logic - method had side effects test needed!
vi.mock('ToolCatalog', () => ({
discoverAndCacheTools: vi.fn().mockResolvedValue(undefined)
}));
await addServer(config);
await addServer(config); // Should throw - but won't!
Fix: Run test with real implementation first, then add minimal mocking.
Anti-Pattern 4: Incomplete Mocks
Bad: Mocking only fields you think you need.
const mockResponse = {
status: 'success',
data: { userId: '123' }
// Missing: metadata that downstream code uses
};
Fix: Mirror the complete real API response structure.
Red Flags
- Assertion checks for
*-mocktest IDs - Methods only called in test files
- Mock setup is >50% of test
- "Mocking just to be safe"
- Test fails when you remove mock
- Regression test cannot fail for the original bug because it bypasses the real call path
Gate Function
Before each mock, ask: "Do I understand what this test actually needs?"
| Question | If No... | |----------|----------| | What side effects does the real method have? | Don't mock yet | | Does this test depend on any of those side effects? | Mock at lower level | | Do I fully understand what this test needs? | Run with real impl first |
Notes
- Only write tests when explicitly requested
- Always run tests after creating them to verify they pass
- Use the project-native test command. If no command is obvious, inspect package scripts, test config, Makefile, or repo docs before guessing.