Skip to content

Commit e1590f9

Browse files
LTSCommerceclaude
andcommitted
✅ Implement type-safe mocking pattern with ESLint enforcement
MAJOR FEATURES: - ✅ Type-safe mocking pattern using production DTOs (CommitDataDTO, RepositoryDataDTO, etc.) - ✅ Custom ESLint rule 'prefer-production-types-in-mocks' for test files - ✅ Comprehensive documentation in docs/Type-Safe-Mocking-Guide.md - ✅ Applied pattern to RepositoryFactCollector and ProjectFactCollector tests TECHNICAL DETAILS: - Replace 'as any' with production types like 'as CommitDataDTO[]' - ESLint rule targets only test files with helpful error messages - Pattern confirmed working: all 91 tests passing - Philosophy: "Mocking is like hot sauce - a little bit is all you need" COMPLETED REFACTOR PHASE 6: Pure fact validation testing - MathematicalCalculator tests (56 tests) - RepositoryFactCollector integration tests (10 tests) - ProjectFactCollector tests (25 tests) - All tests validate no analysis violations 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent be64342 commit e1590f9

12 files changed

Lines changed: 1450 additions & 19 deletions
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
# Type-Safe Mocking Guide
2+
3+
> **Philosophy**: "Mocking is like hot sauce - a little bit is all you need."
4+
5+
## The Problem
6+
7+
Traditional mocking approaches use `any` types, which breaks type safety and hides potential issues:
8+
9+
```typescript
10+
// ❌ WRONG - Unsafe and brittle
11+
vi.mocked(service.getData).mockResolvedValue(mockData as any)
12+
const mockRepo = { name: 'test' } as any
13+
```
14+
15+
## The Solution: Production Type Casting
16+
17+
Use actual production DTOs and interfaces with minimal type casting:
18+
19+
```typescript
20+
// ✅ CORRECT - Type-safe with production types
21+
import { CommitDataDTO, RepositoryDataDTO } from '@/dto'
22+
23+
const mockCommits = [{ sha: 'abc123' }] as CommitDataDTO[]
24+
const mockRepo = { name: 'test-repo', stargazersCount: 42 } as RepositoryDataDTO
25+
26+
vi.mocked(service.searchCommits).mockResolvedValue(mockCommits)
27+
vi.mocked(service.getRepository).mockResolvedValue(mockRepo)
28+
```
29+
30+
## Benefits
31+
32+
1. **Type Safety**: TypeScript enforces compatibility with production interfaces
33+
2. **Minimal Mocking**: Only provide what the test needs
34+
3. **Real Contracts**: Tests validate against actual production types
35+
4. **IDE Support**: Full autocomplete and error checking
36+
5. **Refactor Safe**: Type changes break tests appropriately
37+
38+
## Patterns
39+
40+
### ✅ Arrays of DTOs
41+
```typescript
42+
const mockCommits = [
43+
{ sha: 'abc123', message: 'Initial commit' },
44+
{ sha: 'def456', message: 'Fix bug' }
45+
] as CommitDataDTO[]
46+
47+
vi.mocked(service.searchCommits).mockResolvedValue(mockCommits)
48+
```
49+
50+
### ✅ Complex Objects
51+
```typescript
52+
const mockRepo = {
53+
name: 'test-repo',
54+
stargazersCount: 42,
55+
getAgeInDays: vi.fn().mockReturnValue(365),
56+
getDaysSinceUpdate: vi.fn().mockReturnValue(7)
57+
} as RepositoryDataDTO
58+
59+
vi.mocked(service.getRepository).mockResolvedValue(mockRepo)
60+
```
61+
62+
### ✅ Empty Arrays
63+
```typescript
64+
vi.mocked(service.searchIssues).mockResolvedValue([] as IssueDataDTO[])
65+
```
66+
67+
### ✅ Service Interfaces
68+
```typescript
69+
const mockService: vi.Mocked<IDataService> = {
70+
getData: vi.fn(),
71+
processData: vi.fn()
72+
}
73+
```
74+
75+
## Anti-Patterns
76+
77+
### ❌ Using `any`
78+
```typescript
79+
// NEVER DO THIS
80+
const mockData = { field: 'value' } as any
81+
vi.mocked(service.process).mockResolvedValue(data as any)
82+
```
83+
84+
### ❌ Using `unknown`
85+
```typescript
86+
// AVOID - No better than any for mocking
87+
const mockData = { field: 'value' } as unknown
88+
```
89+
90+
### ❌ Over-Mocking
91+
```typescript
92+
// DON'T CREATE ELABORATE MOCK TYPES
93+
type ComplexMockType = {
94+
// 50 lines of mock-specific types
95+
}
96+
```
97+
98+
### ❌ Mock Types Instead of Production Types
99+
```typescript
100+
// WRONG - Use actual production types, not mock types
101+
type MockCommit = { sha: string } // Don't create this
102+
const mock = {} as MockCommit // Use CommitDataDTO instead
103+
```
104+
105+
## Testing Strategy
106+
107+
Tests should validate production behavior, not mock behavior:
108+
109+
```typescript
110+
// ✅ GOOD - Test real behavior
111+
it('should process commit data correctly', async () => {
112+
const mockCommits = [{ sha: 'abc123' }] as CommitDataDTO[]
113+
vi.mocked(service.getCommits).mockResolvedValue(mockCommits)
114+
115+
const result = await processor.analyzeCommits('owner', 'repo')
116+
117+
expect(result.TOTAL_COMMITS).toBe('1')
118+
expect(result.COMMIT_SHA_LIST).toContain('abc123')
119+
})
120+
```
121+
122+
## ESLint Enforcement
123+
124+
The custom rule `cc-commands/prefer-production-types-in-mocks` enforces this pattern:
125+
126+
- **Detects**: `as any` and `as unknown` in test files
127+
- **Suggests**: Use specific production types instead
128+
- **Scope**: Only applies to `test/**/*.ts` files
129+
130+
## Migration Guide
131+
132+
### From `any` to Production Types
133+
134+
```typescript
135+
// Before
136+
const mockData = { name: 'test' } as any
137+
vi.mocked(service.getData).mockResolvedValue(mockData as any)
138+
139+
// After
140+
const mockData = { name: 'test' } as RepositoryDataDTO
141+
vi.mocked(service.getData).mockResolvedValue(mockData)
142+
```
143+
144+
### From `unknown` to Production Types
145+
146+
```typescript
147+
// Before
148+
vi.mocked(service.getItems).mockResolvedValue([{}] as unknown[])
149+
150+
// After
151+
vi.mocked(service.getItems).mockResolvedValue([{} as ItemDTO])
152+
```
153+
154+
## Rule Configuration
155+
156+
```javascript
157+
// eslint.config.mjs
158+
{
159+
files: ['test/**/*.ts'],
160+
rules: {
161+
'cc-commands/prefer-production-types-in-mocks': 'error'
162+
}
163+
}
164+
```
165+
166+
## Conclusion
167+
168+
Type-safe mocking ensures tests validate real contracts while maintaining development velocity. Use production types, mock minimally, and let TypeScript catch compatibility issues early.

cc-commands-ts/eslint-rules/index.mjs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,15 @@ import noUnsafeTypeCasting from './no-unsafe-type-casting.js'
77
import noStringBasedServiceArgs from './no-string-based-service-args.js'
88
import requireTypedDataAccess from './require-typed-data-access.js'
99
import noApiResponseAny from './no-api-response-any.js'
10+
import preferProductionTypesInMocks from './prefer-production-types-in-mocks.js'
1011

1112
export default {
1213
rules: {
1314
'no-direct-abstract-types': noDirectAbstractTypes,
1415
'no-unsafe-type-casting': noUnsafeTypeCasting,
1516
'no-string-based-service-args': noStringBasedServiceArgs,
1617
'require-typed-data-access': requireTypedDataAccess,
17-
'no-api-response-any': noApiResponseAny
18+
'no-api-response-any': noApiResponseAny,
19+
'prefer-production-types-in-mocks': preferProductionTypesInMocks
1820
}
1921
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
/**
2+
* @fileoverview Enforce production types over any/unknown in test mocks
3+
*
4+
* This rule prevents the use of 'any' and 'unknown' type casting in test files,
5+
* encouraging the use of specific production types for type-safe mocking.
6+
*
7+
* Philosophy: "Mocking is like hot sauce - a little bit is all you need."
8+
* Use actual production DTOs and interfaces with minimal type casting.
9+
*/
10+
11+
export default {
12+
meta: {
13+
type: 'problem',
14+
docs: {
15+
description: 'Enforce production types over any/unknown in test mocks',
16+
category: 'Best Practices',
17+
recommended: true,
18+
},
19+
fixable: null,
20+
schema: [],
21+
messages: {
22+
avoidAnyInTests: 'Avoid using "as any" in test files. Use specific production types instead (e.g., "as CommitDataDTO[]").',
23+
avoidUnknownInTests: 'Avoid using "as unknown" in test files. Use specific production types instead (e.g., "as RepositoryDataDTO").',
24+
avoidAnyUnknownArray: 'Avoid using "as any[]" or "as unknown[]" in test files. Use typed arrays like "as CommitDataDTO[]" instead.',
25+
preferProductionTypes: 'Use production DTOs and interfaces for type-safe mocking. Import the actual types from your codebase.',
26+
},
27+
},
28+
29+
create(context) {
30+
const filename = context.getFilename();
31+
32+
// Only apply this rule to test files
33+
if (!filename.includes('/test/') && !filename.includes('\\test\\') &&
34+
!filename.endsWith('.test.ts') && !filename.endsWith('.test.js') &&
35+
!filename.endsWith('.spec.ts') && !filename.endsWith('.spec.js')) {
36+
return {};
37+
}
38+
39+
return {
40+
TSAsExpression(node) {
41+
const typeAnnotation = node.typeAnnotation;
42+
43+
// Check for "as any"
44+
if (typeAnnotation.type === 'TSAnyKeyword') {
45+
context.report({
46+
node,
47+
messageId: 'avoidAnyInTests',
48+
data: {},
49+
});
50+
return;
51+
}
52+
53+
// Check for "as unknown"
54+
if (typeAnnotation.type === 'TSUnknownKeyword') {
55+
context.report({
56+
node,
57+
messageId: 'avoidUnknownInTests',
58+
data: {},
59+
});
60+
return;
61+
}
62+
63+
// Check for "as any[]" or "as unknown[]"
64+
if (typeAnnotation.type === 'TSArrayType') {
65+
const elementType = typeAnnotation.elementType;
66+
if (elementType.type === 'TSAnyKeyword' || elementType.type === 'TSUnknownKeyword') {
67+
context.report({
68+
node,
69+
messageId: 'avoidAnyUnknownArray',
70+
data: {},
71+
});
72+
return;
73+
}
74+
}
75+
76+
// Check for more complex cases like Array<any> or Array<unknown>
77+
if (typeAnnotation.type === 'TSTypeReference' &&
78+
typeAnnotation.typeName && typeAnnotation.typeName.name === 'Array' &&
79+
typeAnnotation.typeParameters && typeAnnotation.typeParameters.params.length > 0) {
80+
const firstParam = typeAnnotation.typeParameters.params[0];
81+
if (firstParam.type === 'TSAnyKeyword' || firstParam.type === 'TSUnknownKeyword') {
82+
context.report({
83+
node,
84+
messageId: 'avoidAnyUnknownArray',
85+
data: {},
86+
});
87+
return;
88+
}
89+
}
90+
},
91+
92+
// Also catch variable declarations with explicit any/unknown types
93+
TSTypeAnnotation(node) {
94+
const parent = node.parent;
95+
96+
// Only check if we're in a variable declaration or similar context that might be a mock
97+
if (parent && (parent.type === 'VariableDeclarator' || parent.type === 'Parameter')) {
98+
const typeAnnotation = node.typeAnnotation;
99+
100+
if (typeAnnotation.type === 'TSAnyKeyword') {
101+
context.report({
102+
node: parent,
103+
messageId: 'preferProductionTypes',
104+
data: {},
105+
});
106+
}
107+
108+
if (typeAnnotation.type === 'TSUnknownKeyword') {
109+
context.report({
110+
node: parent,
111+
messageId: 'preferProductionTypes',
112+
data: {},
113+
});
114+
}
115+
}
116+
}
117+
};
118+
},
119+
};

cc-commands-ts/eslint.config.mjs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,5 +69,13 @@ export default [
6969
rules: {
7070
'camelcase': 'off' // GitHub API responses use snake_case properties
7171
}
72+
},
73+
{
74+
// Test files - enforce type-safe mocking patterns and relax some rules for mock data
75+
files: ['test/**/*.ts', 'test/**/*.js', '**/*.test.ts', '**/*.spec.ts'],
76+
rules: {
77+
'cc-commands/prefer-production-types-in-mocks': 'error', // Enforce production types over any/unknown in mocks
78+
'camelcase': 'off' // Allow snake_case in test mocks to match API responses
79+
}
7280
}
7381
]

0 commit comments

Comments
 (0)