← Back to Home

How to Create Mock Services for Testing

Updated January 14, 2026
testingmocksvitestdependency injectionunit tests

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

  1. Call tracking: Record all method calls with parameters
  2. Reset method: Clear call history between tests
  3. Configurable behavior: Allow tests to configure responses
  4. Error simulation: Support throwing errors on demand
  5. In-memory storage: Use Maps/Sets instead of file system
  6. Interface compliance: Mocks should implement the same interface
  7. Realistic defaults: Return sensible default data