Creating Mock Services for Testing
Mock services simulate external dependencies in tests, enabling isolated unit testing.
Basic Mock Pattern
import type { IGeminiClient } from '../interfaces/services';
export class MockGeminiClient implements IGeminiClient {
public generateScriptCalls: Array<{ article: FullArticle }> = [];
public generateImageCalls: Array<{ prompt: string }> = [];
public generateAudioCalls: Array<{ text: string }> = [];
async generateScript(article: FullArticle): Promise<VideoScript> {
this.generateScriptCalls.push({ article });
// Return mock data
return {
title: 'Mock Title',
description: 'Mock Description',
totalDuration: 120,
scenes: [
{
sceneNumber: 1,
headline: 'Mock Headline',
narration: 'Mock narration',
visualDescription: 'Mock visual',
duration: 12,
durationInFrames: 360,
},
],
};
}
async generateImage(prompt: string): Promise<Buffer> {
this.generateImageCalls.push({ prompt });
// Return mock image buffer
return Buffer.from('mock-image-data');
}
async generateAudio(text: string): Promise<Buffer> {
this.generateAudioCalls.push({ text });
// Return mock audio buffer
return Buffer.alloc(48000); // 1 second of silence
}
// Reset call tracking
reset(): void {
this.generateScriptCalls = [];
this.generateImageCalls = [];
this.generateAudioCalls = [];
}
}
Using Mocks in Tests
import { describe, it, expect, beforeEach } from 'vitest';
import { AssetGenerationService } from '../services/asset-generation-service';
import { MockGeminiClient } from '../mocks/gemini-client.mock';
describe('AssetGenerationService', () => {
let service: AssetGenerationService;
let mockGemini: MockGeminiClient;
beforeEach(() => {
// Create fresh mocks for each test
mockGemini = new MockGeminiClient();
service = new AssetGenerationService(
mockGemini,
mockAssetManager,
mockArticleManager,
mockAudioService
);
});
it('should generate script, images, and audio for all scenes', async () => {
const article = createMockArticle();
await service.generateAssets(article);
// Verify calls
expect(mockGemini.generateScriptCalls.length).toBe(1);
expect(mockGemini.generateImageCalls.length).toBe(3);
expect(mockGemini.generateAudioCalls.length).toBe(3);
});
});
Configurable Mocks
export class MockAssetManager implements IAssetManager {
private imagesShouldExist = false;
private audioShouldExist = false;
private saveShouldFail = false;
setImagesExist(value: boolean): void {
this.imagesShouldExist = value;
}
setAudioExist(value: boolean): void {
this.audioShouldExist = value;
}
setSaveFails(value: boolean): void {
this.saveShouldFail = value;
}
async imageExists(uuid: string): Promise<boolean> {
return this.imagesShouldExist;
}
async audioExists(uuid: string): Promise<boolean> {
return this.audioShouldExist;
}
async saveImage(uuid: string, buffer: Buffer): Promise<void> {
if (this.saveShouldFail) {
throw new Error('Save failed');
}
}
async saveAudio(uuid: string, buffer: Buffer): Promise<void> {
if (this.saveShouldFail) {
throw new Error('Save failed');
}
}
}
In-Memory Storage Mocks
export class MockArticleManager implements IArticleManager {
private storage = new Map<string, FullArticle>();
async saveArticle(article: FullArticle): Promise<void> {
this.storage.set(article.id, article);
}
async loadArticle(id: string): Promise<FullArticle | null> {
return this.storage.get(id) || null;
}
async listArticles(): Promise<FullArticle[]> {
return Array.from(this.storage.values());
}
async deleteArticle(id: string): Promise<void> {
this.storage.delete(id);
}
// Helper for tests
clear(): void {
this.storage.clear();
}
}
Async Delay Mock
export class MockSlowService implements ISlowService {
private delayMs: number;
constructor(delayMs: number = 0) {
this.delayMs = delayMs;
}
async slowOperation(): Promise<string> {
await new Promise(resolve => setTimeout(resolve, this.delayMs));
return 'result';
}
}
// In tests - use 0 delay for speed
const mockService = new MockSlowService(0);
Error Simulation
export class MockWithError<T extends object> implements T {
private errorOnNextCall = false;
private errorMessage = 'Mock error';
setErrorOnNextCall(message: string = 'Mock error'): void {
this.errorOnNextCall = true;
this.errorMessage = message;
}
protected async checkError(): Promise<void> {
if (this.errorOnNextCall) {
this.errorOnNextCall = false;
throw new Error(this.errorMessage);
}
}
}
// Usage
export class MockGeminiClient extends MockWithError<IGeminiClient> implements IGeminiClient {
async generateScript(article: FullArticle): Promise<VideoScript> {
await this.checkError(); // Throws if error set
// ... normal mock behavior
}
}
Spy Wrapper
export function createSpy<T extends object>(
instance: T,
methodNames: (keyof T)[]
): T {
const spies: Map<keyof T, jest.SpyInstance> = new Map();
methodNames.forEach(methodName => {
const originalMethod = instance[methodName];
if (typeof originalMethod === 'function') {
const spy = jest.fn(originalMethod.bind(instance));
spies.set(methodName, spy);
(instance as any)[methodName] = spy;
}
});
return instance;
}
// Usage
const client = createSpy(new RealGeminiClient(), ['generateScript', 'generateImage']);
await client.generateScript(article);
expect((client as any).generateScript).toHaveBeenCalled();
Test Helper
export function createMockTestContext() {
const mocks = {
gemini: new MockGeminiClient(),
assetManager: new MockAssetManager(),
articleManager: new MockArticleManager(),
audioService: new MockAudioService(),
};
const services = {
assetGeneration: new AssetGenerationService(
mocks.gemini,
mocks.assetManager,
mocks.articleManager,
mocks.audioService
),
};
return {
mocks,
services,
// Reset all mocks
reset(): void {
Object.values(mocks).forEach(mock => {
if (typeof mock.reset === 'function') {
mock.reset();
}
});
},
};
}
// Usage in tests
const { mocks, services, reset } = createMockTestContext();
await services.assetGeneration.generateAssets(article);
expect(mocks.gemini.generateScriptCalls.length).toBe(1);
reset();
Best Practices
- Call tracking: Record all method calls with parameters
- Reset method: Clear call history between tests
- Configurable behavior: Allow tests to configure responses
- Error simulation: Support throwing errors on demand
- In-memory storage: Use Maps/Sets instead of file system
- Interface compliance: Mocks should implement the same interface
- Realistic defaults: Return sensible default data