.agents/skills/test-refactor/SKILL.md
Use when refactoring tests for better maintainability. Provides guidelines for removing duplicates, DRYing fixtures/assertions, restructuring test organization, renaming, and splitting oversized files.
npx skillsauth add tacogips/codex-agent test-refactorInstall 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.
This skill provides guidelines for refactoring test code to improve maintainability, reduce duplication, and establish consistent patterns.
Apply this skill when:
| Pattern | Description | Action | |---------|-------------|--------| | Identical test bodies | Same assertions in multiple tests | Merge or parameterize | | Copy-paste variations | Tests differing only in input values | Convert to parameterized tests | | Redundant coverage | Multiple tests verifying same behavior | Remove redundant tests | | Dead tests | Skipped/disabled tests with no plan | Delete or revive |
// Look for identical describe/it blocks
grep -r "describe\|it\|test" --include="*.test.ts"
// Find similar assertion patterns
grep -r "expect.*toEqual\|expect.*toBe" --include="*.test.ts"
// Identify skipped tests
grep -r "it.skip\|describe.skip\|test.skip\|xit\|xdescribe" --include="*.test.ts"
| Pattern | When to Extract | Target Location |
|---------|-----------------|-----------------|
| Mock objects | Used in 3+ tests | src/test/mocks/ |
| Test data factories | Same shape used repeatedly | src/test/fixtures/ |
| Setup functions | Identical beforeEach blocks | src/test/helpers/ |
| Custom matchers | Same assertion logic repeated | src/test/matchers/ |
src/test/
├── fixtures/ # Test data factories
│ ├── index.ts # Re-exports all fixtures
│ ├── session.ts # Session-related fixtures
│ ├── queue.ts # Queue-related fixtures
│ └── group.ts # Group-related fixtures
├── mocks/ # Mock implementations
│ ├── index.ts # Re-exports all mocks
│ ├── filesystem.ts # File system mocks
│ ├── clock.ts # Time/clock mocks
│ └── process.ts # Process mocks
├── helpers/ # Test utilities
│ ├── index.ts # Re-exports all helpers
│ ├── setup.ts # Common setup functions
│ └── assertions.ts # Custom assertion helpers
└── matchers/ # Custom matchers (if using)
└── index.ts # Custom vitest matchers
// src/test/fixtures/session.ts
export function createTestSession(overrides: Partial<Session> = {}): Session {
return {
id: 'test-session-id',
name: 'Test Session',
createdAt: new Date('2026-01-01T00:00:00Z'),
status: 'active',
...overrides,
};
}
// Usage in tests
const session = createTestSession({ status: 'completed' });
| Anti-Pattern | Refactored Pattern |
|--------------|-------------------|
| Repeated expect(x).toEqual({...long object...}) | Extract expected object to fixture |
| Multiple assertions checking same structure | Create custom matcher or helper |
| Error assertion boilerplate | Create expectError helper |
| Async assertion patterns | Create expectAsync helpers |
// Before (repeated in many tests)
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.error.code).toBe('INVALID_INPUT');
expect(result.error.message).toContain('expected');
}
// After (extracted helper)
// src/test/helpers/assertions.ts
export function expectResultError(
result: Result<unknown, AppError>,
expectedCode: string,
messageContains?: string
): void {
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.error.code).toBe(expectedCode);
if (messageContains) {
expect(result.error.message).toContain(messageContains);
}
}
}
// Usage
expectResultError(result, 'INVALID_INPUT', 'expected');
| Rule | Description |
|------|-------------|
| Colocation | Test file adjacent to source: foo.ts -> foo.test.ts |
| One subject per file | Each test file tests one module/class |
| Logical grouping | Use describe blocks for method/function groups |
| Consistent depth | Max 2-3 levels of describe nesting |
describe('ModuleName', () => {
// Shared setup for all tests
beforeEach(() => { /* common setup */ });
describe('functionName', () => {
describe('when valid input', () => {
it('should return expected result', () => { });
it('should handle edge case', () => { });
});
describe('when invalid input', () => {
it('should return error', () => { });
});
});
describe('anotherFunction', () => {
// ...
});
});
| Component | Convention | Example |
|-----------|------------|---------|
| Test file | <source-file>.test.ts | parser.test.ts |
| describe block | Module/class/function name | describe('Parser', ...) |
| it/test block | should <expected behavior> | it('should parse valid input', ...) |
| Fixture factory | create<Entity> | createTestSession() |
| Mock | Mock<Entity> or mock<Entity> | mockFileSystem |
| Helper | Verb phrase | setupTestEnvironment() |
// GOOD: Clear, specific descriptions
it('should return empty array when no sessions exist', () => { });
it('should throw ValidationError when name exceeds 100 chars', () => { });
it('should emit "completed" event after all tasks finish', () => { });
// BAD: Vague or implementation-focused
it('should work correctly', () => { });
it('should call the function', () => { });
it('test 1', () => { });
| Lines | Priority | Action | |-------|----------|--------| | > 1200 | High | Must split immediately | | 800-1200 | Medium | Should split for maintainability | | 500-800 | Low | Consider splitting if logically separable | | < 500 | N/A | No split needed |
| Strategy | When to Use | Example |
|----------|-------------|---------|
| By Feature | Tests cover multiple distinct features | user-api.test.ts -> user-api-auth.test.ts, user-api-profile.test.ts |
| By Test Type | Mix of unit/integration/e2e | service.test.ts -> service.unit.test.ts, service.integration.test.ts |
| By Module Method | Many methods tested in one file | parser.test.ts -> parser-tokenize.test.ts, parser-parse.test.ts |
| By Scenario | Large success/error case groups | api.test.ts -> api-success.test.ts, api-errors.test.ts |
// src/services/__tests__/api-service.setup.ts
import { vi } from 'vitest';
export function setupApiServiceTests() {
const mockServer = vi.fn();
const mockDatabase = vi.fn();
beforeEach(() => {
vi.clearAllMocks();
});
return { mockServer, mockDatabase };
}
export const testFixtures = {
validUser: { id: '1', name: 'Test User' },
invalidUser: { id: '', name: '' },
};
// src/services/api-service-user.test.ts
import { setupApiServiceTests, testFixtures } from './__tests__/api-service.setup';
describe('UserAPI', () => {
const { mockServer, mockDatabase } = setupApiServiceTests();
it('should create user', () => {
// test using shared mocks and fixtures
});
});
| Original File | Split Files |
|--------------|-------------|
| service.test.ts | service-feature1.test.ts, service-feature2.test.ts |
| api.test.ts | api-user.test.ts, api-product.test.ts, api-order.test.ts |
| parser.test.ts | parser.unit.test.ts, parser.integration.test.ts |
src/test/fixtures/src/test/helpers/Before completing refactoring:
| Anti-Pattern | Why It's Bad | Better Approach | |--------------|--------------|-----------------| | Over-abstraction | Tests become hard to understand | Keep some duplication if it aids clarity | | Shared mutable state | Tests become interdependent | Fresh fixtures per test | | Complex setup chains | Hard to trace test prerequisites | Explicit setup in each test | | Testing implementation | Tests break on refactoring | Test behavior, not implementation | | Magic fixtures | Unclear what's being tested | Explicit inline data for key values |
| Extraction | Target Location | Import Pattern |
|------------|-----------------|----------------|
| Mock objects | src/test/mocks/<domain>.ts | import { mockX } from '@/test/mocks' |
| Test data | src/test/fixtures/<domain>.ts | import { createX } from '@/test/fixtures' |
| Setup helpers | src/test/helpers/setup.ts | import { setupX } from '@/test/helpers' |
| Assertions | src/test/helpers/assertions.ts | import { expectX } from '@/test/helpers' |
// Convert multiple similar tests to parameterized test
describe('validation', () => {
const testCases = [
{ input: '', expected: false, description: 'empty string' },
{ input: 'valid', expected: true, description: 'valid input' },
{ input: ' ', expected: false, description: 'whitespace only' },
];
it.each(testCases)(
'should return $expected for $description',
({ input, expected }) => {
expect(isValid(input)).toBe(expected);
}
);
});
development
Use when writing, reviewing, or refactoring TypeScript code. Provides type safety patterns, error handling, project layout, and async programming guidelines.
testing
Use when creating test plans from implementation and design documents. Provides test plan structure, test case tracking, and coverage guidelines.
development
Use when creating, publishing, or maintaining npm packages with Bun. Provides Shai-Hulud supply chain attack countermeasures including npm token management, 2FA enforcement, provenance signing, trusted publishing via GitHub Actions, and pre-publish security checklists.
development
Use when installing, updating, or auditing npm dependencies with Bun. Provides Shai-Hulud supply chain attack countermeasures including bunfig.toml hardening, lockfile verification, trustedDependencies management, and CI/CD pipeline security.