Cypress cy.intercept: APIモック完全ガイド

Shunku

cy.intercept()はCypressの最も強力な機能の1つで、ネットワークリクエストをインターセプトして制御できます。これにより、より高速なテスト、決定論的なレスポンス、実際のAPIでは再現が難しいエッジケースのテストが可能になります。

なぜネットワークリクエストをモックするのか?

flowchart TD
    subgraph Real["実際のAPIコール"]
        A[遅いテスト]
        B[ネットワークで不安定]
        C[エッジケースのテストが困難]
        D[テストデータのセットアップが必要]
    end

    subgraph Mocked["cy.interceptでモック"]
        E[高速なテスト]
        F[決定論的な結果]
        G[エラーテストが容易]
        H[バックエンド不要]
    end

    style Real fill:#ef4444,color:#fff
    style Mocked fill:#10b981,color:#fff

cy.interceptの基本的な使い方

GETリクエストのインターセプト

// インターセプトしてレスポンスをスタブ
cy.intercept('GET', '/api/users', {
  body: [
    { id: 1, name: 'Alice' },
    { id: 2, name: 'Bob' },
  ],
});

cy.visit('/users');
cy.get('.user').should('have.length', 2);

URLパターンマッチング

// 完全一致URL
cy.intercept('GET', '/api/users');

// Globパターン
cy.intercept('GET', '/api/users/*');
cy.intercept('GET', '/api/*/comments');
cy.intercept('GET', '**/users');

// 正規表現パターン
cy.intercept('GET', /\/api\/users\/\d+/);

// クエリパラメータ付き(glob)
cy.intercept('GET', '/api/users?page=*');

メソッドショートカット

// 明示的なメソッド
cy.intercept('GET', '/api/users', { body: [] });
cy.intercept('POST', '/api/users', { statusCode: 201 });
cy.intercept('PUT', '/api/users/*', { statusCode: 200 });
cy.intercept('DELETE', '/api/users/*', { statusCode: 204 });

// 任意のメソッド
cy.intercept('/api/users'); // すべてのメソッドにマッチ

レスポンスのスタブ

静的レスポンス

// JSONボディを返す
cy.intercept('GET', '/api/users', {
  body: [{ id: 1, name: 'Alice' }],
});

// 完全なレスポンスオブジェクト
cy.intercept('GET', '/api/users', {
  statusCode: 200,
  body: [{ id: 1, name: 'Alice' }],
  headers: {
    'Content-Type': 'application/json',
    'X-Custom-Header': 'value',
  },
});

フィクスチャの使用

// cypress/fixtures/users.json
// [{ "id": 1, "name": "Alice" }, { "id": 2, "name": "Bob" }]

// フィクスチャをレスポンスとして読み込む
cy.intercept('GET', '/api/users', { fixture: 'users.json' });

// ステータスコード付き
cy.intercept('GET', '/api/users', {
  statusCode: 200,
  fixture: 'users.json',
});

// ネストしたフィクスチャ
cy.intercept('GET', '/api/users', { fixture: 'api/users/list.json' });

ルートハンドラーで動的レスポンス

// 動的にレスポンスを変更
cy.intercept('GET', '/api/users', (req) => {
  req.reply({
    statusCode: 200,
    body: [{ id: 1, name: 'Dynamic User' }],
  });
});

// リクエストデータにアクセス
cy.intercept('POST', '/api/users', (req) => {
  const { name, email } = req.body;

  req.reply({
    statusCode: 201,
    body: { id: Date.now(), name, email },
  });
});

// 条件付きレスポンス
cy.intercept('GET', '/api/users/*', (req) => {
  const userId = req.url.split('/').pop();

  if (userId === '999') {
    req.reply({ statusCode: 404, body: { error: 'Not found' } });
  } else {
    req.reply({ body: { id: userId, name: 'User ' + userId } });
  }
});

リクエストの待機

エイリアスの使用

// エイリアスを作成
cy.intercept('GET', '/api/users').as('getUsers');

cy.visit('/users');

// リクエストを待機
cy.wait('@getUsers');

// ロード後にアサート
cy.get('.user').should('have.length', 2);

リクエスト/レスポンスデータへのアクセス

cy.intercept('POST', '/api/users').as('createUser');

cy.get('[data-testid="name"]').type('John');
cy.get('[data-testid="submit"]').click();

cy.wait('@createUser').then((interception) => {
  // リクエストにアクセス
  expect(interception.request.body).to.have.property('name', 'John');
  expect(interception.request.headers).to.have.property('authorization');

  // レスポンスにアクセス
  expect(interception.response.statusCode).to.equal(201);
  expect(interception.response.body).to.have.property('id');
});

待機とアサーション

cy.intercept('GET', '/api/users').as('getUsers');

cy.visit('/users');

// 1ステップで待機とアサート
cy.wait('@getUsers').its('response.statusCode').should('eq', 200);

// 複数のアサーション
cy.wait('@getUsers').should((interception) => {
  expect(interception.response.body).to.have.length.greaterThan(0);
  expect(interception.response.headers['content-type']).to.include('application/json');
});

複数回の待機

cy.intercept('GET', '/api/items/*').as('getItem');

// ページネーション - 各ページリクエストを待機
cy.wait('@getItem');
cy.get('.next-page').click();
cy.wait('@getItem');
cy.get('.next-page').click();
cy.wait('@getItem');

// または特定の回数を待機
cy.intercept('POST', '/api/analytics').as('analytics');
cy.wait(['@analytics', '@analytics', '@analytics']); // 3回のコールを待機

エラーテスト

ネットワークエラー

// ネットワーク障害をシミュレート
cy.intercept('GET', '/api/users', { forceNetworkError: true });

cy.visit('/users');
cy.get('[data-testid="error"]').should('contain', 'ネットワークエラー');

HTTPエラーレスポンス

// 404 Not Found
cy.intercept('GET', '/api/users/999', {
  statusCode: 404,
  body: { error: 'ユーザーが見つかりません' },
});

// 500 Server Error
cy.intercept('GET', '/api/users', {
  statusCode: 500,
  body: { error: '内部サーバーエラー' },
});

// 401 Unauthorized
cy.intercept('GET', '/api/users', {
  statusCode: 401,
  body: { error: '認証が必要です' },
});

// 422 Validation Error
cy.intercept('POST', '/api/users', {
  statusCode: 422,
  body: {
    errors: {
      email: ['このメールアドレスは既に使用されています'],
      name: ['名前は必須です'],
    },
  },
});

エラーUIのテスト

describe('エラーハンドリング', () => {
  it('500でエラーメッセージを表示', () => {
    cy.intercept('GET', '/api/users', {
      statusCode: 500,
      body: { error: 'Server error' },
    });

    cy.visit('/users');

    cy.get('[data-testid="error-message"]')
      .should('be.visible')
      .and('contain', '問題が発生しました');

    cy.get('[data-testid="retry-button"]').should('be.visible');
  });

  it('404でNot Foundページを表示', () => {
    cy.intercept('GET', '/api/users/999', { statusCode: 404 });

    cy.visit('/users/999');

    cy.get('[data-testid="not-found"]').should('be.visible');
  });
});

レスポンスの遅延

遅いネットワークのシミュレート

// レスポンスを2秒遅延
cy.intercept('GET', '/api/users', {
  body: [{ id: 1, name: 'Alice' }],
  delay: 2000,
});

// ローディング状態をテスト
cy.visit('/users');
cy.get('[data-testid="loading"]').should('be.visible');
cy.get('[data-testid="loading"]').should('not.exist');
cy.get('.user').should('have.length', 1);

ローディング状態のテスト

it('フェッチ中にローディングスピナーを表示', () => {
  cy.intercept('GET', '/api/users', {
    body: [],
    delay: 1000,
  });

  cy.visit('/users');

  // 最初はローディングが表示
  cy.get('[data-testid="spinner"]').should('be.visible');

  // データロード後にローディングが消える
  cy.get('[data-testid="spinner"]').should('not.exist');
  cy.get('[data-testid="empty-state"]').should('be.visible');
});

リクエストの変更

送信リクエストの変更

cy.intercept('GET', '/api/users', (req) => {
  // ヘッダーを追加
  req.headers['X-Custom-Header'] = 'test-value';

  // 実際のサーバーに続行(スタブなし)
  req.continue();
});

// クエリパラメータを変更
cy.intercept('GET', '/api/users*', (req) => {
  req.url = req.url + '&limit=100';
  req.continue();
});

レスポンスの変更

cy.intercept('GET', '/api/users', (req) => {
  req.continue((res) => {
    // レスポンスボディを変更
    res.body = res.body.map((user) => ({
      ...user,
      name: user.name.toUpperCase(),
    }));

    // ヘッダーを変更
    res.headers['X-Modified'] = 'true';
  });
});

リクエストのスパイ

スタブなしでスパイ

// 変更なしでリクエストを監視
cy.intercept('POST', '/api/analytics').as('analytics');

cy.visit('/');
cy.get('button').click();

cy.wait('@analytics').then((interception) => {
  expect(interception.request.body).to.deep.include({
    event: 'button_click',
  });
});

リクエストボディのアサート

cy.intercept('POST', '/api/users').as('createUser');

cy.get('[data-testid="name"]').type('John Doe');
cy.get('[data-testid="email"]').type('john@example.com');
cy.get('[data-testid="submit"]').click();

cy.wait('@createUser').its('request.body').should('deep.equal', {
  name: 'John Doe',
  email: 'john@example.com',
});

テストパターン

認証フロー

describe('認証', () => {
  it('ログインしてトークンを保存', () => {
    cy.intercept('POST', '/api/login', {
      body: { token: 'fake-jwt-token', user: { id: 1, name: 'John' } },
    }).as('login');

    cy.visit('/login');
    cy.get('[data-testid="email"]').type('john@example.com');
    cy.get('[data-testid="password"]').type('password');
    cy.get('[data-testid="submit"]').click();

    cy.wait('@login');
    cy.url().should('include', '/dashboard');
  });

  it('認証済みリクエストでトークンを送信', () => {
    cy.window().then((win) => {
      win.localStorage.setItem('token', 'fake-jwt-token');
    });

    cy.intercept('GET', '/api/profile', (req) => {
      expect(req.headers).to.have.property('authorization', 'Bearer fake-jwt-token');
      req.reply({ body: { id: 1, name: 'John' } });
    }).as('getProfile');

    cy.visit('/profile');
    cy.wait('@getProfile');
  });
});

ページネーション

describe('ページネーション', () => {
  it('スクロールで追加アイテムを読み込む', () => {
    cy.intercept('GET', '/api/items?page=1', {
      fixture: 'items-page1.json',
    }).as('page1');

    cy.intercept('GET', '/api/items?page=2', {
      fixture: 'items-page2.json',
    }).as('page2');

    cy.visit('/items');
    cy.wait('@page1');
    cy.get('.item').should('have.length', 10);

    cy.scrollTo('bottom');
    cy.wait('@page2');
    cy.get('.item').should('have.length', 20);
  });
});

まとめ

機能 使い方
cy.intercept(method, url) リクエストをインターセプト
{ body, statusCode } レスポンスをスタブ
{ fixture: 'file.json' } フィクスチャからレスポンス
{ delay: 1000 } 遅いネットワークをシミュレート
{ forceNetworkError: true } ネットワーク障害をシミュレート
.as('alias') 待機用のエイリアスを作成
cy.wait('@alias') リクエストを待機
req.continue() サーバーに通過

重要なポイント:

  • テストでネットワークリクエストを制御するにはcy.intercept()を使用
  • より高速で決定論的なテストのためにレスポンスをスタブ
  • 複雑なレスポンスデータにはフィクスチャを使用
  • 異なるステータスコードでエラー状態をテスト
  • エイリアスとcy.wait()でリクエストと同期
  • ローディング状態をテストするためにレスポンスを遅延
  • リクエストデータを検証するためにスタブなしでスパイ

cy.intercept()によるネットワークモックは、テストをより高速で信頼性が高く、実際のAPIコールでは難しいエッジケースをカバーできるようにします。

参考文献