T-CREATOR

Jest の mockImplementation 活用事例集

Jest の mockImplementation 活用事例集

実際の開発現場では、外部 API やデータベース、ファイルシステムなど、様々な外部依存を持つコードをテストする必要があります。こうした場面で威力を発揮するのが Jest の mockImplementation です。

単純なモック関数とは異なり、mockImplementation は動的な戻り値制御や複雑な条件分岐、エラーシナリオの再現など、実際のビジネスロジックに近い形でテストを実行できます。本記事では、開発者の皆様が日常的に遭遇する具体的なシナリオを通じて、mockImplementation の実践的な活用方法をご紹介いたします。

各事例では実際のコード例とともに、なぜその手法が効果的なのか、どのような課題を解決できるのかも詳しく解説してまいります。テストの品質向上と開発効率の改善に、ぜひお役立てください。

外部 API 通信のモック事例

REST API の成功・失敗パターン

外部 API との通信をテストする際、ネットワークの状態やサーバーの応答を制御する必要があります。以下は典型的な REST API クライアントのテスト例です。

typescript// api/userService.ts
interface User {
  id: number;
  name: string;
  email: string;
}

export class UserService {
  async fetchUser(id: number): Promise<User> {
    const response = await fetch(`/api/users/${id}`);

    if (!response.ok) {
      throw new Error(
        `Failed to fetch user: ${response.status}`
      );
    }

    return response.json();
  }

  async createUser(
    userData: Omit<User, 'id'>
  ): Promise<User> {
    const response = await fetch('/api/users', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(userData),
    });

    if (!response.ok) {
      throw new Error(
        `Failed to create user: ${response.status}`
      );
    }

    return response.json();
  }
}

上記のサービスクラスをテストする際、実際の API サーバーにアクセスするのは現実的ではありません。mockImplementation を使用して、様々なレスポンスパターンをシミュレートしましょう。

typescript// __tests__/userService.test.ts
import { UserService } from '../api/userService';

// fetch関数をモック化
global.fetch = jest.fn();
const mockFetch = fetch as jest.MockedFunction<
  typeof fetch
>;

describe('UserService', () => {
  let userService: UserService;

  beforeEach(() => {
    userService = new UserService();
    mockFetch.mockClear();
  });

  describe('fetchUser', () => {
    it('正常なレスポンスの場合、ユーザー情報を返す', async () => {
      const mockUser = {
        id: 1,
        name: '田中太郎',
        email: 'tanaka@example.com',
      };

      // 成功レスポンスをモック
      mockFetch.mockImplementation(() =>
        Promise.resolve({
          ok: true,
          json: () => Promise.resolve(mockUser),
        } as Response)
      );

      const result = await userService.fetchUser(1);

      expect(result).toEqual(mockUser);
      expect(mockFetch).toHaveBeenCalledWith(
        '/api/users/1'
      );
    });

    it('404エラーの場合、適切なエラーをスローする', async () => {
      // エラーレスポンスをモック
      mockFetch.mockImplementation(() =>
        Promise.resolve({
          ok: false,
          status: 404,
        } as Response)
      );

      await expect(
        userService.fetchUser(999)
      ).rejects.toThrow('Failed to fetch user: 404');
    });

    it('ネットワークエラーの場合、エラーをスローする', async () => {
      // ネットワークエラーをモック
      mockFetch.mockImplementation(() =>
        Promise.reject(new Error('Network error'))
      );

      await expect(
        userService.fetchUser(1)
      ).rejects.toThrow('Network error');
    });
  });
});

GraphQL クエリのモック

GraphQL を使用している場合も、同様の手法でクエリの結果をモックできます。

typescript// api/graphqlClient.ts
import {
  ApolloClient,
  gql,
  InMemoryCache,
} from '@apollo/client';

const GET_USER = gql`
  query GetUser($id: ID!) {
    user(id: $id) {
      id
      name
      email
      posts {
        id
        title
      }
    }
  }
`;

export class GraphQLUserService {
  constructor(private client: ApolloClient<any>) {}

  async getUser(id: string) {
    const result = await this.client.query({
      query: GET_USER,
      variables: { id },
    });

    return result.data.user;
  }
}

GraphQL クライアントのテストでは、クエリの実行結果をモックします。

typescript// __tests__/graphqlUserService.test.ts
import { ApolloClient } from '@apollo/client';
import { GraphQLUserService } from '../api/graphqlClient';

describe('GraphQLUserService', () => {
  let mockClient: jest.Mocked<ApolloClient<any>>;
  let service: GraphQLUserService;

  beforeEach(() => {
    // ApolloClientをモック
    mockClient = {
      query: jest.fn(),
    } as any;

    service = new GraphQLUserService(mockClient);
  });

  it('ユーザー情報を正常に取得できる', async () => {
    const mockUserData = {
      id: '1',
      name: '佐藤花子',
      email: 'sato@example.com',
      posts: [
        { id: '1', title: '初投稿です' },
        { id: '2', title: 'Jest学習記録' },
      ],
    };

    // GraphQLクエリの結果をモック
    mockClient.query.mockImplementation(() =>
      Promise.resolve({
        data: { user: mockUserData },
        loading: false,
        networkStatus: 7,
      })
    );

    const result = await service.getUser('1');

    expect(result).toEqual(mockUserData);
    expect(mockClient.query).toHaveBeenCalledWith({
      query: expect.any(Object),
      variables: { id: '1' },
    });
  });

  it('ユーザーが見つからない場合、nullを返す', async () => {
    mockClient.query.mockImplementation(() =>
      Promise.resolve({
        data: { user: null },
        loading: false,
        networkStatus: 7,
      })
    );

    const result = await service.getUser('999');
    expect(result).toBeNull();
  });
});

ファイルシステム操作のモック事例

ファイル読み書きのモック

Node.js アプリケーションでファイルシステムを操作するコードをテストする際、実際のファイルを作成・削除するのは望ましくありません。fs モジュールをモックして、様々なファイル操作をシミュレートしましょう。

typescript// utils/fileManager.ts
import { promises as fs } from 'fs';
import path from 'path';

export class FileManager {
  async readConfig(configPath: string): Promise<any> {
    try {
      const content = await fs.readFile(
        configPath,
        'utf-8'
      );
      return JSON.parse(content);
    } catch (error) {
      if (
        (error as NodeJS.ErrnoException).code === 'ENOENT'
      ) {
        throw new Error(
          `設定ファイルが見つかりません: ${configPath}`
        );
      }
      throw error;
    }
  }

  async saveLog(
    logDir: string,
    message: string
  ): Promise<void> {
    const timestamp = new Date()
      .toISOString()
      .split('T')[0];
    const logFile = path.join(
      logDir,
      `app-${timestamp}.log`
    );

    await fs.mkdir(logDir, { recursive: true });
    await fs.appendFile(
      logFile,
      `${new Date().toISOString()}: ${message}\n`
    );
  }

  async backupFile(
    sourcePath: string,
    backupDir: string
  ): Promise<string> {
    const fileName = path.basename(sourcePath);
    const timestamp = Date.now();
    const backupPath = path.join(
      backupDir,
      `${timestamp}-${fileName}`
    );

    await fs.mkdir(backupDir, { recursive: true });
    await fs.copyFile(sourcePath, backupPath);

    return backupPath;
  }
}

ファイル操作のテストでは、fs モジュール全体をモックし、各メソッドの動作を制御します。

typescript// __tests__/fileManager.test.ts
import { promises as fs } from 'fs';
import { FileManager } from '../utils/fileManager';

// fsモジュールをモック
jest.mock('fs', () => ({
  promises: {
    readFile: jest.fn(),
    writeFile: jest.fn(),
    appendFile: jest.fn(),
    mkdir: jest.fn(),
    copyFile: jest.fn(),
  },
}));

const mockFs = fs as jest.Mocked<typeof fs>;

describe('FileManager', () => {
  let fileManager: FileManager;

  beforeEach(() => {
    fileManager = new FileManager();
    jest.clearAllMocks();
  });

  describe('readConfig', () => {
    it('設定ファイルを正常に読み込める', async () => {
      const mockConfig = {
        database: { host: 'localhost', port: 5432 },
      };

      mockFs.readFile.mockImplementation(() =>
        Promise.resolve(JSON.stringify(mockConfig))
      );

      const result = await fileManager.readConfig(
        '/path/to/config.json'
      );

      expect(result).toEqual(mockConfig);
      expect(mockFs.readFile).toHaveBeenCalledWith(
        '/path/to/config.json',
        'utf-8'
      );
    });

    it('ファイルが存在しない場合、適切なエラーをスローする', async () => {
      const error = new Error(
        'File not found'
      ) as NodeJS.ErrnoException;
      error.code = 'ENOENT';

      mockFs.readFile.mockImplementation(() =>
        Promise.reject(error)
      );

      await expect(
        fileManager.readConfig('/nonexistent/config.json')
      ).rejects.toThrow('設定ファイルが見つかりません');
    });

    it('JSON解析エラーの場合、エラーをスローする', async () => {
      mockFs.readFile.mockImplementation(() =>
        Promise.resolve('invalid json content')
      );

      await expect(
        fileManager.readConfig('/path/to/config.json')
      ).rejects.toThrow();
    });
  });

  describe('saveLog', () => {
    it('ログディレクトリを作成してログを保存する', async () => {
      mockFs.mkdir.mockImplementation(() =>
        Promise.resolve(undefined)
      );
      mockFs.appendFile.mockImplementation(() =>
        Promise.resolve()
      );

      await fileManager.saveLog(
        '/logs',
        'テストメッセージ'
      );

      expect(mockFs.mkdir).toHaveBeenCalledWith('/logs', {
        recursive: true,
      });
      expect(mockFs.appendFile).toHaveBeenCalledWith(
        expect.stringMatching(
          /\/logs\/app-\d{4}-\d{2}-\d{2}\.log/
        ),
        expect.stringContaining('テストメッセージ')
      );
    });
  });

  describe('backupFile', () => {
    it('ファイルをバックアップディレクトリにコピーする', async () => {
      mockFs.mkdir.mockImplementation(() =>
        Promise.resolve(undefined)
      );
      mockFs.copyFile.mockImplementation(() =>
        Promise.resolve()
      );

      // 固定の時刻をモック
      const mockDate = new Date('2024-01-15T10:30:00Z');
      jest
        .spyOn(Date, 'now')
        .mockReturnValue(mockDate.getTime());

      const backupPath = await fileManager.backupFile(
        '/source/file.txt',
        '/backup'
      );

      expect(backupPath).toBe(
        `/backup/${mockDate.getTime()}-file.txt`
      );
      expect(mockFs.mkdir).toHaveBeenCalledWith('/backup', {
        recursive: true,
      });
      expect(mockFs.copyFile).toHaveBeenCalledWith(
        '/source/file.txt',
        backupPath
      );
    });
  });
});

時間・日付に依存する処理のモック事例

Date オブジェクトのモック

時間に依存するロジックをテストする際、現在時刻を固定値でモックすることで、再現可能なテストを作成できます。

typescript// utils/timeService.ts
export class TimeService {
  getCurrentTimestamp(): number {
    return Date.now();
  }

  formatCurrentDate(): string {
    return new Date().toISOString().split('T')[0];
  }

  isBusinessHour(): boolean {
    const now = new Date();
    const hour = now.getHours();
    const day = now.getDay();

    // 平日の9時〜18時をビジネスアワーとする
    return day >= 1 && day <= 5 && hour >= 9 && hour < 18;
  }

  getTimeUntilNextBusinessDay(): number {
    const now = new Date();
    const nextBusinessDay = new Date(now);

    // 現在が金曜日の場合、月曜日まで
    // それ以外は翌日まで
    if (now.getDay() === 5) {
      // 金曜日
      nextBusinessDay.setDate(now.getDate() + 3);
    } else if (now.getDay() === 6) {
      // 土曜日
      nextBusinessDay.setDate(now.getDate() + 2);
    } else if (now.getDay() === 0) {
      // 日曜日
      nextBusinessDay.setDate(now.getDate() + 1);
    } else {
      // 平日
      nextBusinessDay.setDate(now.getDate() + 1);
    }

    nextBusinessDay.setHours(9, 0, 0, 0);
    return nextBusinessDay.getTime() - now.getTime();
  }
}

時間依存のテストでは、Date コンストラクタや Date.now() をモックして、特定の日時でのテストを実行します。

typescript// __tests__/timeService.test.ts
import { TimeService } from '../utils/timeService';

describe('TimeService', () => {
  let timeService: TimeService;
  let mockDate: jest.SpyInstance;

  beforeEach(() => {
    timeService = new TimeService();
  });

  afterEach(() => {
    if (mockDate) {
      mockDate.mockRestore();
    }
  });

  describe('getCurrentTimestamp', () => {
    it('現在のタイムスタンプを返す', () => {
      const fixedTime = new Date(
        '2024-01-15T10:30:00Z'
      ).getTime();

      // Date.nowをモック
      jest.spyOn(Date, 'now').mockReturnValue(fixedTime);

      const result = timeService.getCurrentTimestamp();
      expect(result).toBe(fixedTime);
    });
  });

  describe('formatCurrentDate', () => {
    it('現在日付をYYYY-MM-DD形式で返す', () => {
      const fixedDate = new Date('2024-01-15T10:30:00Z');

      // Dateコンストラクタをモック
      mockDate = jest
        .spyOn(global, 'Date')
        .mockImplementation(() => fixedDate);

      const result = timeService.formatCurrentDate();
      expect(result).toBe('2024-01-15');
    });
  });

  describe('isBusinessHour', () => {
    it('平日の営業時間内の場合、trueを返す', () => {
      // 2024年1月15日(月曜日)の14時をモック
      const businessHourDate = new Date(
        '2024-01-15T14:00:00Z'
      );
      mockDate = jest
        .spyOn(global, 'Date')
        .mockImplementation(() => businessHourDate);

      const result = timeService.isBusinessHour();
      expect(result).toBe(true);
    });

    it('平日の営業時間外の場合、falseを返す', () => {
      // 2024年1月15日(月曜日)の20時をモック
      const afterHourDate = new Date(
        '2024-01-15T20:00:00Z'
      );
      mockDate = jest
        .spyOn(global, 'Date')
        .mockImplementation(() => afterHourDate);

      const result = timeService.isBusinessHour();
      expect(result).toBe(false);
    });

    it('週末の場合、falseを返す', () => {
      // 2024年1月13日(土曜日)の14時をモック
      const weekendDate = new Date('2024-01-13T14:00:00Z');
      mockDate = jest
        .spyOn(global, 'Date')
        .mockImplementation(() => weekendDate);

      const result = timeService.isBusinessHour();
      expect(result).toBe(false);
    });
  });

  describe('getTimeUntilNextBusinessDay', () => {
    it('金曜日の場合、月曜日の9時までの時間を返す', () => {
      // 2024年1月12日(金曜日)の15時をモック
      const fridayDate = new Date('2024-01-12T15:00:00Z');
      mockDate = jest
        .spyOn(global, 'Date')
        .mockImplementation(() => fridayDate);

      const result =
        timeService.getTimeUntilNextBusinessDay();

      // 月曜日の9時まで(3日と18時間 = 66時間)
      const expectedMs = 66 * 60 * 60 * 1000;
      expect(result).toBe(expectedMs);
    });

    it('平日の場合、翌日の9時までの時間を返す', () => {
      // 2024年1月15日(月曜日)の15時をモック
      const mondayDate = new Date('2024-01-15T15:00:00Z');
      mockDate = jest
        .spyOn(global, 'Date')
        .mockImplementation(() => mondayDate);

      const result =
        timeService.getTimeUntilNextBusinessDay();

      // 翌日の9時まで(18時間)
      const expectedMs = 18 * 60 * 60 * 1000;
      expect(result).toBe(expectedMs);
    });
  });
});

setTimeout・setInterval のモック

タイマー機能を含むコードをテストする際は、Jest のタイマーモック機能と mockImplementation を組み合わせます。

typescript// utils/scheduler.ts
export class Scheduler {
  private timers: Set<NodeJS.Timeout> = new Set();

  scheduleTask(
    callback: () => void,
    delay: number
  ): NodeJS.Timeout {
    const timer = setTimeout(() => {
      callback();
      this.timers.delete(timer);
    }, delay);

    this.timers.add(timer);
    return timer;
  }

  scheduleRecurringTask(
    callback: () => void,
    interval: number
  ): NodeJS.Timeout {
    const timer = setInterval(callback, interval);
    this.timers.add(timer);
    return timer;
  }

  cancelAllTasks(): void {
    this.timers.forEach((timer) => {
      clearTimeout(timer);
      clearInterval(timer);
    });
    this.timers.clear();
  }

  getActiveTaskCount(): number {
    return this.timers.size;
  }
}

タイマーを使用するコードのテストでは、Jest のタイマーモック機能を活用します。

typescript// __tests__/scheduler.test.ts
import { Scheduler } from '../utils/scheduler';

describe('Scheduler', () => {
  let scheduler: Scheduler;

  beforeEach(() => {
    scheduler = new Scheduler();
    // Jestのタイマーモックを有効化
    jest.useFakeTimers();
  });

  afterEach(() => {
    scheduler.cancelAllTasks();
    // タイマーモックをクリア
    jest.clearAllTimers();
    jest.useRealTimers();
  });

  describe('scheduleTask', () => {
    it('指定した遅延後にタスクが実行される', () => {
      const mockCallback = jest.fn();

      scheduler.scheduleTask(mockCallback, 1000);

      // 500ms経過時点ではまだ実行されない
      jest.advanceTimersByTime(500);
      expect(mockCallback).not.toHaveBeenCalled();

      // 1000ms経過時点で実行される
      jest.advanceTimersByTime(500);
      expect(mockCallback).toHaveBeenCalledTimes(1);
    });

    it('タスク実行後、アクティブなタスク数が減る', () => {
      const mockCallback = jest.fn();

      scheduler.scheduleTask(mockCallback, 1000);
      expect(scheduler.getActiveTaskCount()).toBe(1);

      // タスク実行
      jest.advanceTimersByTime(1000);
      expect(scheduler.getActiveTaskCount()).toBe(0);
    });
  });

  describe('scheduleRecurringTask', () => {
    it('指定した間隔で繰り返しタスクが実行される', () => {
      const mockCallback = jest.fn();

      scheduler.scheduleRecurringTask(mockCallback, 500);

      // 3回分の時間を進める
      jest.advanceTimersByTime(1500);

      expect(mockCallback).toHaveBeenCalledTimes(3);
    });
  });

  describe('cancelAllTasks', () => {
    it('全てのタスクがキャンセルされる', () => {
      const mockCallback1 = jest.fn();
      const mockCallback2 = jest.fn();

      scheduler.scheduleTask(mockCallback1, 1000);
      scheduler.scheduleRecurringTask(mockCallback2, 500);

      expect(scheduler.getActiveTaskCount()).toBe(2);

      scheduler.cancelAllTasks();
      expect(scheduler.getActiveTaskCount()).toBe(0);

      // 時間を進めてもコールバックは実行されない
      jest.advanceTimersByTime(2000);
      expect(mockCallback1).not.toHaveBeenCalled();
      expect(mockCallback2).not.toHaveBeenCalled();
    });
  });
});

条件分岐とエラーハンドリングのモック事例

複雑な条件分岐のテスト

ビジネスロジックには複数の条件分岐が含まれることが多く、全てのパスをテストするために mockImplementation で様々な状況をシミュレートする必要があります。

typescript// services/orderService.ts
interface Product {
  id: string;
  name: string;
  price: number;
  stock: number;
}

interface User {
  id: string;
  membershipLevel: 'bronze' | 'silver' | 'gold';
  points: number;
}

export class OrderService {
  constructor(
    private productRepository: any,
    private userRepository: any,
    private paymentService: any
  ) {}

  async calculateOrderTotal(
    userId: string,
    productId: string,
    quantity: number
  ): Promise<{
    subtotal: number;
    discount: number;
    total: number;
    pointsUsed: number;
  }> {
    // ユーザー情報を取得
    const user = await this.userRepository.findById(userId);
    if (!user) {
      throw new Error('ユーザーが見つかりません');
    }

    // 商品情報を取得
    const product = await this.productRepository.findById(
      productId
    );
    if (!product) {
      throw new Error('商品が見つかりません');
    }

    // 在庫チェック
    if (product.stock < quantity) {
      throw new Error('在庫が不足しています');
    }

    const subtotal = product.price * quantity;
    let discount = 0;
    let pointsUsed = 0;

    // 会員レベル別割引
    switch (user.membershipLevel) {
      case 'gold':
        discount = subtotal * 0.15; // 15%割引
        break;
      case 'silver':
        discount = subtotal * 0.1; // 10%割引
        break;
      case 'bronze':
        discount = subtotal * 0.05; // 5%割引
        break;
    }

    // 高額商品の追加割引(10,000円以上)
    if (subtotal >= 10000) {
      discount += 500;
    }

    // ポイント使用(最大で小計の50%まで)
    const maxPointsUsable = Math.floor(subtotal * 0.5);
    pointsUsed = Math.min(user.points, maxPointsUsable);

    const total = subtotal - discount - pointsUsed;

    return {
      subtotal,
      discount,
      total: Math.max(total, 0),
      pointsUsed,
    };
  }

  async processPayment(
    userId: string,
    productId: string,
    quantity: number
  ): Promise<{ success: boolean; orderId?: string }> {
    try {
      const orderTotal = await this.calculateOrderTotal(
        userId,
        productId,
        quantity
      );

      if (orderTotal.total <= 0) {
        throw new Error('注文金額が無効です');
      }

      // 支払い処理
      const paymentResult =
        await this.paymentService.charge(orderTotal.total);

      if (!paymentResult.success) {
        throw new Error(
          paymentResult.error || '支払い処理に失敗しました'
        );
      }

      return {
        success: true,
        orderId: paymentResult.transactionId,
      };
    } catch (error) {
      return {
        success: false,
      };
    }
  }
}

このサービスクラスをテストする際、様々な条件をシミュレートして全ての分岐をカバーする必要があります。

typescript// __tests__/orderService.test.ts
import { OrderService } from '../services/orderService';

describe('OrderService', () => {
  let orderService: OrderService;
  let mockProductRepository: any;
  let mockUserRepository: any;
  let mockPaymentService: any;

  beforeEach(() => {
    mockProductRepository = {
      findById: jest.fn(),
    };
    mockUserRepository = {
      findById: jest.fn(),
    };
    mockPaymentService = {
      charge: jest.fn(),
    };

    orderService = new OrderService(
      mockProductRepository,
      mockUserRepository,
      mockPaymentService
    );
  });

  describe('calculateOrderTotal', () => {
    it('ゴールド会員で高額商品の場合、最大割引が適用される', async () => {
      const mockUser = {
        id: 'user1',
        membershipLevel: 'gold' as const,
        points: 2000,
      };
      const mockProduct = {
        id: 'product1',
        name: 'ラップトップ',
        price: 50000,
        stock: 10,
      };

      mockUserRepository.findById.mockImplementation(
        (userId: string) => {
          if (userId === 'user1')
            return Promise.resolve(mockUser);
          return Promise.resolve(null);
        }
      );

      mockProductRepository.findById.mockImplementation(
        (productId: string) => {
          if (productId === 'product1')
            return Promise.resolve(mockProduct);
          return Promise.resolve(null);
        }
      );

      const result = await orderService.calculateOrderTotal(
        'user1',
        'product1',
        1
      );

      expect(result.subtotal).toBe(50000);
      expect(result.discount).toBe(8000); // 15% + 500円
      expect(result.pointsUsed).toBe(2000);
      expect(result.total).toBe(40000);
    });

    it('ユーザーが存在しない場合、エラーをスローする', async () => {
      mockUserRepository.findById.mockImplementation(() =>
        Promise.resolve(null)
      );

      await expect(
        orderService.calculateOrderTotal(
          'nonexistent',
          'product1',
          1
        )
      ).rejects.toThrow('ユーザーが見つかりません');
    });

    it('商品が存在しない場合、エラーをスローする', async () => {
      const mockUser = {
        id: 'user1',
        membershipLevel: 'bronze' as const,
        points: 0,
      };

      mockUserRepository.findById.mockImplementation(() =>
        Promise.resolve(mockUser)
      );
      mockProductRepository.findById.mockImplementation(
        () => Promise.resolve(null)
      );

      await expect(
        orderService.calculateOrderTotal(
          'user1',
          'nonexistent',
          1
        )
      ).rejects.toThrow('商品が見つかりません');
    });

    it('在庫不足の場合、エラーをスローする', async () => {
      const mockUser = {
        id: 'user1',
        membershipLevel: 'bronze' as const,
        points: 0,
      };
      const mockProduct = {
        id: 'product1',
        name: 'ラップトップ',
        price: 50000,
        stock: 1,
      };

      mockUserRepository.findById.mockImplementation(() =>
        Promise.resolve(mockUser)
      );
      mockProductRepository.findById.mockImplementation(
        () => Promise.resolve(mockProduct)
      );

      await expect(
        orderService.calculateOrderTotal(
          'user1',
          'product1',
          5
        )
      ).rejects.toThrow('在庫が不足しています');
    });
  });

  describe('processPayment', () => {
    it('正常な支払い処理の場合、成功レスポンスを返す', async () => {
      const mockUser = {
        id: 'user1',
        membershipLevel: 'silver' as const,
        points: 1000,
      };
      const mockProduct = {
        id: 'product1',
        name: 'ラップトップ',
        price: 30000,
        stock: 10,
      };

      mockUserRepository.findById.mockImplementation(() =>
        Promise.resolve(mockUser)
      );
      mockProductRepository.findById.mockImplementation(
        () => Promise.resolve(mockProduct)
      );
      mockPaymentService.charge.mockImplementation(
        (amount: number) =>
          Promise.resolve({
            success: true,
            transactionId: 'txn_123456',
          })
      );

      const result = await orderService.processPayment(
        'user1',
        'product1',
        1
      );

      expect(result.success).toBe(true);
      expect(result.orderId).toBe('txn_123456');
    });

    it('支払い処理が失敗した場合、エラーレスポンスを返す', async () => {
      const mockUser = {
        id: 'user1',
        membershipLevel: 'bronze' as const,
        points: 0,
      };
      const mockProduct = {
        id: 'product1',
        name: 'ラップトップ',
        price: 30000,
        stock: 10,
      };

      mockUserRepository.findById.mockImplementation(() =>
        Promise.resolve(mockUser)
      );
      mockProductRepository.findById.mockImplementation(
        () => Promise.resolve(mockProduct)
      );
      mockPaymentService.charge.mockImplementation(() =>
        Promise.resolve({
          success: false,
          error: 'カードが拒否されました',
        })
      );

      const result = await orderService.processPayment(
        'user1',
        'product1',
        1
      );

      expect(result.success).toBe(false);
      expect(result.orderId).toBeUndefined();
    });
  });
});

React コンポーネントの props・hooks モック事例

カスタムフックのモック

React アプリケーションでカスタムフックを使用している場合、そのフックの戻り値をモックして様々な状態をテストできます。

typescript// hooks/useUserData.ts
import { useState, useEffect } from 'react';

interface User {
  id: string;
  name: string;
  email: string;
}

export const useUserData = (userId: string) => {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const fetchUser = async () => {
      try {
        setLoading(true);
        const response = await fetch(
          `/api/users/${userId}`
        );
        if (!response.ok) {
          throw new Error('ユーザーの取得に失敗しました');
        }
        const userData = await response.json();
        setUser(userData);
      } catch (err) {
        setError(
          err instanceof Error
            ? err.message
            : '不明なエラー'
        );
      } finally {
        setLoading(false);
      }
    };

    if (userId) {
      fetchUser();
    }
  }, [userId]);

  return { user, loading, error };
};
typescript// components/UserProfile.tsx
import React from 'react';
import { useUserData } from '../hooks/useUserData';

interface UserProfileProps {
  userId: string;
}

export const UserProfile: React.FC<UserProfileProps> = ({
  userId,
}) => {
  const { user, loading, error } = useUserData(userId);

  if (loading) {
    return <div data-testid='loading'>読み込み中...</div>;
  }

  if (error) {
    return (
      <div data-testid='error' role='alert'>
        エラー: {error}
      </div>
    );
  }

  if (!user) {
    return (
      <div data-testid='no-user'>
        ユーザーが見つかりません
      </div>
    );
  }

  return (
    <div data-testid='user-profile'>
      <h2>{user.name}</h2>
      <p>メール: {user.email}</p>
    </div>
  );
};

カスタムフックをモックして、様々な状態をテストします。

typescript// __tests__/UserProfile.test.tsx
import React from 'react';
import { render, screen } from '@testing-library/react';
import { UserProfile } from '../components/UserProfile';
import { useUserData } from '../hooks/useUserData';

// カスタムフックをモック
jest.mock('../hooks/useUserData');
const mockUseUserData = useUserData as jest.MockedFunction<
  typeof useUserData
>;

describe('UserProfile', () => {
  it('ローディング状態を表示する', () => {
    mockUseUserData.mockImplementation(() => ({
      user: null,
      loading: true,
      error: null,
    }));

    render(<UserProfile userId='user123' />);

    expect(
      screen.getByTestId('loading')
    ).toBeInTheDocument();
    expect(
      screen.getByText('読み込み中...')
    ).toBeInTheDocument();
  });

  it('ユーザー情報を表示する', () => {
    const mockUser = {
      id: 'user123',
      name: '田中太郎',
      email: 'tanaka@example.com',
    };

    mockUseUserData.mockImplementation(() => ({
      user: mockUser,
      loading: false,
      error: null,
    }));

    render(<UserProfile userId='user123' />);

    expect(
      screen.getByTestId('user-profile')
    ).toBeInTheDocument();
    expect(
      screen.getByText('田中太郎')
    ).toBeInTheDocument();
    expect(
      screen.getByText('メール: tanaka@example.com')
    ).toBeInTheDocument();
  });

  it('エラー状態を表示する', () => {
    mockUseUserData.mockImplementation(() => ({
      user: null,
      loading: false,
      error: 'ネットワークエラーです',
    }));

    render(<UserProfile userId='user123' />);

    expect(screen.getByTestId('error')).toBeInTheDocument();
    expect(
      screen.getByText('エラー: ネットワークエラーです')
    ).toBeInTheDocument();
    expect(screen.getByRole('alert')).toBeInTheDocument();
  });

  it('ユーザーが見つからない場合の表示', () => {
    mockUseUserData.mockImplementation(() => ({
      user: null,
      loading: false,
      error: null,
    }));

    render(<UserProfile userId='nonexistent' />);

    expect(
      screen.getByTestId('no-user')
    ).toBeInTheDocument();
    expect(
      screen.getByText('ユーザーが見つかりません')
    ).toBeInTheDocument();
  });
});

Node.js バックエンド処理のモック事例

データベース操作のモック

Node.js アプリケーションでデータベースアクセスをテストする際、実際のデータベースではなくモックを使用します。

typescript// repositories/userRepository.ts
import { Pool } from 'pg';

interface User {
  id: number;
  name: string;
  email: string;
  created_at: Date;
}

export class UserRepository {
  constructor(private db: Pool) {}

  async findById(id: number): Promise<User | null> {
    const query = 'SELECT * FROM users WHERE id = $1';
    const result = await this.db.query(query, [id]);

    if (result.rows.length === 0) {
      return null;
    }

    return result.rows[0];
  }

  async create(name: string, email: string): Promise<User> {
    const query = `
      INSERT INTO users (name, email, created_at)
      VALUES ($1, $2, NOW())
      RETURNING *
    `;
    const result = await this.db.query(query, [
      name,
      email,
    ]);
    return result.rows[0];
  }

  async updateEmail(
    id: number,
    newEmail: string
  ): Promise<User | null> {
    const query = `
      UPDATE users 
      SET email = $1 
      WHERE id = $2 
      RETURNING *
    `;
    const result = await this.db.query(query, [
      newEmail,
      id,
    ]);

    if (result.rows.length === 0) {
      return null;
    }

    return result.rows[0];
  }

  async delete(id: number): Promise<boolean> {
    const query = 'DELETE FROM users WHERE id = $1';
    const result = await this.db.query(query, [id]);
    return result.rowCount > 0;
  }
}

データベース操作のテストでは、データベースクライアントをモックします。

typescript// __tests__/userRepository.test.ts
import { Pool } from 'pg';
import { UserRepository } from '../repositories/userRepository';

// pgモジュールをモック
jest.mock('pg', () => ({
  Pool: jest.fn().mockImplementation(() => ({
    query: jest.fn(),
  })),
}));

describe('UserRepository', () => {
  let userRepository: UserRepository;
  let mockDb: jest.Mocked<Pool>;

  beforeEach(() => {
    mockDb = new Pool() as jest.Mocked<Pool>;
    userRepository = new UserRepository(mockDb);
  });

  describe('findById', () => {
    it('ユーザーが存在する場合、ユーザー情報を返す', async () => {
      const mockUser = {
        id: 1,
        name: '田中太郎',
        email: 'tanaka@example.com',
        created_at: new Date('2024-01-15'),
      };

      mockDb.query.mockImplementation((query, params) => {
        if (
          query === 'SELECT * FROM users WHERE id = $1' &&
          params?.[0] === 1
        ) {
          return Promise.resolve({
            rows: [mockUser],
            rowCount: 1,
          } as any);
        }
        return Promise.resolve({
          rows: [],
          rowCount: 0,
        } as any);
      });

      const result = await userRepository.findById(1);

      expect(result).toEqual(mockUser);
      expect(mockDb.query).toHaveBeenCalledWith(
        'SELECT * FROM users WHERE id = $1',
        [1]
      );
    });

    it('ユーザーが存在しない場合、nullを返す', async () => {
      mockDb.query.mockImplementation(() =>
        Promise.resolve({ rows: [], rowCount: 0 } as any)
      );

      const result = await userRepository.findById(999);

      expect(result).toBeNull();
    });
  });

  describe('create', () => {
    it('新しいユーザーを作成する', async () => {
      const mockCreatedUser = {
        id: 1,
        name: '鈴木花子',
        email: 'suzuki@example.com',
        created_at: new Date('2024-01-15'),
      };

      mockDb.query.mockImplementation((query, params) => {
        if (
          query.includes('INSERT INTO users') &&
          params?.[0] === '鈴木花子' &&
          params?.[1] === 'suzuki@example.com'
        ) {
          return Promise.resolve({
            rows: [mockCreatedUser],
            rowCount: 1,
          } as any);
        }
        return Promise.resolve({
          rows: [],
          rowCount: 0,
        } as any);
      });

      const result = await userRepository.create(
        '鈴木花子',
        'suzuki@example.com'
      );

      expect(result).toEqual(mockCreatedUser);
      expect(mockDb.query).toHaveBeenCalledWith(
        expect.stringContaining('INSERT INTO users'),
        ['鈴木花子', 'suzuki@example.com']
      );
    });
  });

  describe('updateEmail', () => {
    it('メールアドレスを更新する', async () => {
      const mockUpdatedUser = {
        id: 1,
        name: '田中太郎',
        email: 'new-tanaka@example.com',
        created_at: new Date('2024-01-15'),
      };

      mockDb.query.mockImplementation(() =>
        Promise.resolve({
          rows: [mockUpdatedUser],
          rowCount: 1,
        } as any)
      );

      const result = await userRepository.updateEmail(
        1,
        'new-tanaka@example.com'
      );

      expect(result).toEqual(mockUpdatedUser);
    });

    it('存在しないユーザーの場合、nullを返す', async () => {
      mockDb.query.mockImplementation(() =>
        Promise.resolve({ rows: [], rowCount: 0 } as any)
      );

      const result = await userRepository.updateEmail(
        999,
        'new@example.com'
      );

      expect(result).toBeNull();
    });
  });

  describe('delete', () => {
    it('ユーザーを削除する', async () => {
      mockDb.query.mockImplementation(() =>
        Promise.resolve({ rowCount: 1 } as any)
      );

      const result = await userRepository.delete(1);

      expect(result).toBe(true);
      expect(mockDb.query).toHaveBeenCalledWith(
        'DELETE FROM users WHERE id = $1',
        [1]
      );
    });

    it('存在しないユーザーの場合、falseを返す', async () => {
      mockDb.query.mockImplementation(() =>
        Promise.resolve({ rowCount: 0 } as any)
      );

      const result = await userRepository.delete(999);

      expect(result).toBe(false);
    });
  });
});

Redis キャッシュのモック

キャッシュサーバーとの連携をテストする際も、同様にモックを活用します。

typescript// services/cacheService.ts
import Redis from 'ioredis';

export class CacheService {
  constructor(private redis: Redis) {}

  async get<T>(key: string): Promise<T | null> {
    const value = await this.redis.get(key);
    if (!value) return null;

    try {
      return JSON.parse(value);
    } catch {
      return value as unknown as T;
    }
  }

  async set(
    key: string,
    value: any,
    expireInSeconds?: number
  ): Promise<void> {
    const serializedValue = JSON.stringify(value);

    if (expireInSeconds) {
      await this.redis.setex(
        key,
        expireInSeconds,
        serializedValue
      );
    } else {
      await this.redis.set(key, serializedValue);
    }
  }

  async delete(key: string): Promise<boolean> {
    const result = await this.redis.del(key);
    return result > 0;
  }

  async exists(key: string): Promise<boolean> {
    const result = await this.redis.exists(key);
    return result === 1;
  }
}
typescript// __tests__/cacheService.test.ts
import Redis from 'ioredis';
import { CacheService } from '../services/cacheService';

// ioredisモジュールをモック
jest.mock('ioredis');

describe('CacheService', () => {
  let cacheService: CacheService;
  let mockRedis: jest.Mocked<Redis>;

  beforeEach(() => {
    mockRedis = new Redis() as jest.Mocked<Redis>;
    cacheService = new CacheService(mockRedis);
  });

  describe('get', () => {
    it('キャッシュされた値を取得する', async () => {
      const mockData = { id: 1, name: '田中太郎' };
      mockRedis.get.mockImplementation((key) => {
        if (key === 'user:1') {
          return Promise.resolve(JSON.stringify(mockData));
        }
        return Promise.resolve(null);
      });

      const result = await cacheService.get('user:1');

      expect(result).toEqual(mockData);
      expect(mockRedis.get).toHaveBeenCalledWith('user:1');
    });

    it('キャッシュが存在しない場合、nullを返す', async () => {
      mockRedis.get.mockImplementation(() =>
        Promise.resolve(null)
      );

      const result = await cacheService.get('nonexistent');

      expect(result).toBeNull();
    });
  });

  describe('set', () => {
    it('有効期限付きでキャッシュを設定する', async () => {
      const testData = { id: 1, name: '田中太郎' };
      mockRedis.setex.mockImplementation(() =>
        Promise.resolve('OK')
      );

      await cacheService.set('user:1', testData, 3600);

      expect(mockRedis.setex).toHaveBeenCalledWith(
        'user:1',
        3600,
        JSON.stringify(testData)
      );
    });

    it('有効期限なしでキャッシュを設定する', async () => {
      const testData = { id: 1, name: '田中太郎' };
      mockRedis.set.mockImplementation(() =>
        Promise.resolve('OK')
      );

      await cacheService.set('user:1', testData);

      expect(mockRedis.set).toHaveBeenCalledWith(
        'user:1',
        JSON.stringify(testData)
      );
    });
  });

  describe('delete', () => {
    it('キャッシュを削除する', async () => {
      mockRedis.del.mockImplementation(() =>
        Promise.resolve(1)
      );

      const result = await cacheService.delete('user:1');

      expect(result).toBe(true);
      expect(mockRedis.del).toHaveBeenCalledWith('user:1');
    });

    it('存在しないキーの場合、falseを返す', async () => {
      mockRedis.del.mockImplementation(() =>
        Promise.resolve(0)
      );

      const result = await cacheService.delete(
        'nonexistent'
      );

      expect(result).toBe(false);
    });
  });

  describe('exists', () => {
    it('キーが存在する場合、trueを返す', async () => {
      mockRedis.exists.mockImplementation(() =>
        Promise.resolve(1)
      );

      const result = await cacheService.exists('user:1');

      expect(result).toBe(true);
    });

    it('キーが存在しない場合、falseを返す', async () => {
      mockRedis.exists.mockImplementation(() =>
        Promise.resolve(0)
      );

      const result = await cacheService.exists(
        'nonexistent'
      );

      expect(result).toBe(false);
    });
  });
});

非同期処理とプロミスのモック事例

Promise.all と Promise.race のモック

複数の非同期処理を並行実行するコードをテストする際、個々のプロミスの結果をコントロールしてテストします。

typescript// services/batchProcessingService.ts
export class BatchProcessingService {
  constructor(
    private apiClient: any,
    private logger: any
  ) {}

  async processUsersInParallel(userIds: string[]): Promise<{
    results: Array<{
      userId: string;
      success: boolean;
      data?: any;
      error?: string;
    }>;
    summary: {
      total: number;
      success: number;
      failed: number;
    };
  }> {
    const promises = userIds.map(async (userId) => {
      try {
        const userData = await this.apiClient.fetchUser(
          userId
        );
        const processedData =
          await this.apiClient.processUser(userData);

        this.logger.info(
          `ユーザー ${userId} の処理が完了しました`
        );

        return {
          userId,
          success: true,
          data: processedData,
        };
      } catch (error) {
        this.logger.error(
          `ユーザー ${userId} の処理でエラー: ${error}`
        );

        return {
          userId,
          success: false,
          error:
            error instanceof Error
              ? error.message
              : 'Unknown error',
        };
      }
    });

    const results = await Promise.all(promises);

    const summary = {
      total: results.length,
      success: results.filter((r) => r.success).length,
      failed: results.filter((r) => !r.success).length,
    };

    return { results, summary };
  }

  async processWithTimeout(
    userIds: string[],
    timeoutMs: number
  ): Promise<{
    completed: boolean;
    results?: any;
    error?: string;
  }> {
    const processingPromise =
      this.processUsersInParallel(userIds);

    const timeoutPromise = new Promise((_, reject) =>
      setTimeout(
        () =>
          reject(new Error('処理がタイムアウトしました')),
        timeoutMs
      )
    );

    try {
      const results = await Promise.race([
        processingPromise,
        timeoutPromise,
      ]);
      return { completed: true, results };
    } catch (error) {
      return {
        completed: false,
        error:
          error instanceof Error
            ? error.message
            : 'Unknown error',
      };
    }
  }

  async processWithRetry(
    userId: string,
    maxRetries: number = 3
  ): Promise<{
    success: boolean;
    data?: any;
    retriesUsed: number;
  }> {
    let retriesUsed = 0;

    for (
      let attempt = 0;
      attempt <= maxRetries;
      attempt++
    ) {
      try {
        const userData = await this.apiClient.fetchUser(
          userId
        );
        const processedData =
          await this.apiClient.processUser(userData);

        return {
          success: true,
          data: processedData,
          retriesUsed,
        };
      } catch (error) {
        retriesUsed++;

        if (attempt === maxRetries) {
          return {
            success: false,
            retriesUsed,
          };
        }

        // 指数バックオフで待機
        const waitTime = Math.pow(2, attempt) * 1000;
        await new Promise((resolve) =>
          setTimeout(resolve, waitTime)
        );
      }
    }

    return { success: false, retriesUsed };
  }
}
typescript// __tests__/batchProcessingService.test.ts
import { BatchProcessingService } from '../services/batchProcessingService';

describe('BatchProcessingService', () => {
  let batchProcessingService: BatchProcessingService;
  let mockApiClient: any;
  let mockLogger: any;

  beforeEach(() => {
    mockApiClient = {
      fetchUser: jest.fn(),
      processUser: jest.fn(),
    };
    mockLogger = {
      info: jest.fn(),
      error: jest.fn(),
    };

    batchProcessingService = new BatchProcessingService(
      mockApiClient,
      mockLogger
    );

    // タイマーモックを有効化
    jest.useFakeTimers();
  });

  afterEach(() => {
    jest.useRealTimers();
  });

  describe('processUsersInParallel', () => {
    it('全てのユーザーが正常に処理される場合', async () => {
      const userIds = ['user1', 'user2', 'user3'];

      mockApiClient.fetchUser.mockImplementation(
        (userId: string) => {
          return Promise.resolve({
            id: userId,
            name: `User ${userId}`,
          });
        }
      );

      mockApiClient.processUser.mockImplementation(
        (userData: any) => {
          return Promise.resolve({
            processed: true,
            userId: userData.id,
          });
        }
      );

      const result =
        await batchProcessingService.processUsersInParallel(
          userIds
        );

      expect(result.summary.total).toBe(3);
      expect(result.summary.success).toBe(3);
      expect(result.summary.failed).toBe(0);
      expect(result.results).toHaveLength(3);
      expect(result.results.every((r) => r.success)).toBe(
        true
      );
    });

    it('一部のユーザー処理が失敗する場合', async () => {
      const userIds = ['user1', 'user2', 'user3'];

      mockApiClient.fetchUser.mockImplementation(
        (userId: string) => {
          if (userId === 'user2') {
            return Promise.reject(
              new Error('ユーザーが見つかりません')
            );
          }
          return Promise.resolve({
            id: userId,
            name: `User ${userId}`,
          });
        }
      );

      mockApiClient.processUser.mockImplementation(
        (userData: any) => {
          return Promise.resolve({
            processed: true,
            userId: userData.id,
          });
        }
      );

      const result =
        await batchProcessingService.processUsersInParallel(
          userIds
        );

      expect(result.summary.total).toBe(3);
      expect(result.summary.success).toBe(2);
      expect(result.summary.failed).toBe(1);

      const failedResult = result.results.find(
        (r) => r.userId === 'user2'
      );
      expect(failedResult?.success).toBe(false);
      expect(failedResult?.error).toBe(
        'ユーザーが見つかりません'
      );
    });
  });

  describe('processWithTimeout', () => {
    it('タイムアウト時間内に処理が完了する場合', async () => {
      const userIds = ['user1'];

      mockApiClient.fetchUser.mockImplementation(() =>
        Promise.resolve({ id: 'user1', name: 'User 1' })
      );
      mockApiClient.processUser.mockImplementation(() =>
        Promise.resolve({ processed: true })
      );

      const resultPromise =
        batchProcessingService.processWithTimeout(
          userIds,
          5000
        );

      // 少し時間を進める(タイムアウトより短い)
      jest.advanceTimersByTime(1000);

      const result = await resultPromise;

      expect(result.completed).toBe(true);
      expect(result.results).toBeDefined();
      expect(result.error).toBeUndefined();
    });

    it('タイムアウトが発生する場合', async () => {
      const userIds = ['user1'];

      // 処理が非常に遅いAPIをモック
      mockApiClient.fetchUser.mockImplementation(
        () =>
          new Promise((resolve) =>
            setTimeout(
              () => resolve({ id: 'user1' }),
              10000
            )
          )
      );

      const resultPromise =
        batchProcessingService.processWithTimeout(
          userIds,
          2000
        );

      // タイムアウト時間を経過させる
      jest.advanceTimersByTime(2000);

      const result = await resultPromise;

      expect(result.completed).toBe(false);
      expect(result.error).toBe(
        '処理がタイムアウトしました'
      );
      expect(result.results).toBeUndefined();
    });
  });

  describe('processWithRetry', () => {
    it('最初の試行で成功する場合', async () => {
      mockApiClient.fetchUser.mockImplementation(() =>
        Promise.resolve({ id: 'user1', name: 'User 1' })
      );
      mockApiClient.processUser.mockImplementation(() =>
        Promise.resolve({ processed: true })
      );

      const result =
        await batchProcessingService.processWithRetry(
          'user1',
          3
        );

      expect(result.success).toBe(true);
      expect(result.retriesUsed).toBe(0);
      expect(result.data).toEqual({ processed: true });
    });

    it('リトライ後に成功する場合', async () => {
      let attemptCount = 0;

      mockApiClient.fetchUser.mockImplementation(() => {
        attemptCount++;
        if (attemptCount <= 2) {
          return Promise.reject(
            new Error('一時的なエラー')
          );
        }
        return Promise.resolve({
          id: 'user1',
          name: 'User 1',
        });
      });

      mockApiClient.processUser.mockImplementation(() =>
        Promise.resolve({ processed: true })
      );

      const resultPromise =
        batchProcessingService.processWithRetry('user1', 3);

      // 指数バックオフの待機時間をスキップ
      jest.runAllTimers();

      const result = await resultPromise;

      expect(result.success).toBe(true);
      expect(result.retriesUsed).toBe(2);
      expect(attemptCount).toBe(3);
    });

    it('最大リトライ回数を超えて失敗する場合', async () => {
      mockApiClient.fetchUser.mockImplementation(() =>
        Promise.reject(new Error('恒久的なエラー'))
      );

      const resultPromise =
        batchProcessingService.processWithRetry('user1', 2);

      // 全ての待機時間をスキップ
      jest.runAllTimers();

      const result = await resultPromise;

      expect(result.success).toBe(false);
      expect(result.retriesUsed).toBe(2);
      expect(result.data).toBeUndefined();
    });
  });
});

まとめ

この記事では、Jest の mockImplementation を活用した実践的な事例を 7 つの領域に分けて解説しました。

外部 API の通信からファイルシステム操作、時間依存の処理、複雑な条件分岐、React コンポーネント、Node.js バックエンド、非同期処理まで、様々なシナリオでのモック活用法をご紹介しました。

mockImplementation の真価は、実際のコードでは制御が困難な外部依存関係を自在に操ることで、確実で高速なテストを実現できる点にあります。特に以下のようなケースで威力を発揮しますね。

  • エラーケースの網羅的なテスト: ネットワークエラーやファイルアクセスエラーなど、本来再現が困難なエラー状況を簡単にシミュレートできます
  • 時間に依存する処理の安定化: 現在時刻やタイマーを固定値でモックすることで、テスト実行時間に関係なく一貫した結果を得られます
  • 外部サービスからの独立: データベースや外部 API に依存せず、高速で信頼性の高いテストを作成できます

実際のプロジェクトでは、これらの事例を参考にしながら、皆さんのアプリケーションの特性に合わせてモック戦略を調整していってください。適切なモックの活用により、開発効率の向上とコードの品質確保を両立できるはずです。

関連リンク