--- name: typescript-test-doubles description: Test doubles with Vitest (vi.fn, vi.mock, vi.spyOn) and Mother pattern. Use when creating test data, mocking dependencies, or setting up module mocks in TypeScript tests. --- # Test Doubles (TypeScript) This skill defines how to use test doubles in TypeScript (Vitest). --- ## Test Doubles Types ### 1. Mock: vi.fn() Use `vi.fn()` to create mock functions for port interfaces. **Purpose:** Replace real dependencies with controllable test doubles. ```typescript const mockProvider: ISecretProvider = { getSecret: vi.fn(), }; const mockStore: IVariableStore = { getMapping: vi.fn(), saveEnvironment: vi.fn(), }; const mockLogger: ILogger = { info: vi.fn(), warn: vi.fn(), error: vi.fn(), }; ``` ### 2. Stub: mockResolvedValue / mockReturnValue Use return value methods to configure stubs. **Purpose:** Predefined responses without caring about call verification. ```typescript // Async stub mockProvider.getSecret.mockResolvedValue('secret-value'); mockStore.getMapping.mockResolvedValue({ KEY: '/ssm/path' }); // Sync stub mockParser.parse.mockReturnValue({ config: {}, mappings: {} }); // Sequential returns mockProvider.getSecret .mockResolvedValueOnce('first') .mockResolvedValueOnce('second'); ``` ### 3. Spy: vi.spyOn() Use `vi.spyOn()` to observe calls on real objects without replacing behavior. **Purpose:** Verify interactions while preserving real implementation. ```typescript const spy = vi.spyOn(console, 'log'); sut.execute(); expect(spy).toHaveBeenCalledWith('Processing...'); spy.mockRestore(); ``` ### 4. Module Mock: vi.mock() Use `vi.mock()` to replace entire modules. **Purpose:** Replace external dependencies (AWS SDK, file system, etc.). ```typescript vi.mock('@aws-sdk/client-ssm', () => ({ SSMClient: vi.fn().mockImplementation(() => ({ send: vi.fn(), })), GetParameterCommand: vi.fn(), })); ``` ### 5. Error Simulation: mockRejectedValue Use `mockRejectedValue()` to simulate failures. **Purpose:** Test error paths and exception handling. ```typescript mockProvider.getSecret.mockRejectedValue( new ParameterNotFoundError('/missing/key'), ); mockStore.getMapping.mockRejectedValue( new Error('File not found'), ); ``` --- ## Verification Patterns ### Basic Verification ```typescript expect(mockProvider.getSecret).toHaveBeenCalledWith('/ssm/path'); expect(mockProvider.getSecret).toHaveBeenCalledTimes(1); expect(mockLogger.info).toHaveBeenCalledWith( expect.stringContaining('success'), ); ``` ### Not Called ```typescript expect(mockStore.saveEnvironment).not.toHaveBeenCalled(); expect(mockLogger.error).not.toHaveBeenCalled(); ``` ### Call Order ```typescript expect(mockProvider.getSecret).toHaveBeenCalledBefore( mockStore.saveEnvironment, ); ``` ### Argument Matchers ```typescript expect(mockLogger.info).toHaveBeenCalledWith( expect.stringMatching(/loaded \d+ secrets/), ); expect(mockStore.saveEnvironment).toHaveBeenCalledWith( expect.objectContaining({ KEY: 'value' }), ); ``` --- ## Mother Pattern Use factory functions for reusable test data creation. ```typescript function createParsedMapFile( overrides: Partial = {}, ): ParsedMapFile { return { config: { provider: 'aws' }, mappings: new Map([['DB_URL', '/app/db']]), ...overrides, }; } function createEnvilderOptions( overrides: Partial = {}, ): EnvilderOptions { return { provider: SecretProviderType.AWS, profile: undefined, vaultUrl: undefined, ...overrides, }; } ``` Usage: ```typescript // Arrange const mapFile = createParsedMapFile({ mappings: new Map([['API_KEY', '/prod/api-key']]), }); ``` --- ## Port Test Double Pattern Build complete mock objects implementing domain interfaces: ```typescript function createMockSecretProvider(): ISecretProvider & { getSecrets: Mock; } { return { getSecrets: vi.fn().mockResolvedValue(new Map()), }; } function createMockLogger(): ILogger & { info: Mock; warn: Mock; error: Mock; } { return { info: vi.fn(), warn: vi.fn(), error: vi.fn(), }; } ``` --- ## Summary | Double Type | Vitest API | Purpose | | ----------- | ---------- | ------- | | Mock | `vi.fn()` | Controllable replacement | | Stub | `.mockResolvedValue()` / `.mockReturnValue()` | Predefined responses | | Spy | `vi.spyOn()` | Observe real objects | | Module mock | `vi.mock()` | Replace entire modules | | Error sim | `.mockRejectedValue()` | Failure paths | When writing tests: 1. Create port test doubles with `vi.fn()` in `beforeEach` 2. Configure stubs with `.mockResolvedValue()` in Arrange 3. **Always verify** mock interactions in Assert 4. Use Mother pattern for complex test data 5. Prefer `vi.fn()` over `vi.mock()` (port-level > module-level)