コンテンツにスキップ

Cypress@E2E テスト

はじめに

本サイトにつきまして、以下をご認識のほど宜しくお願いいたします。


01. Cypress とは

E2E テストに必要な機能を提供し、テストを実行する。


02. 文法

data-cy

Cypress のためだけに UI レンダリングロジックにつける要素のこと。

export function SubmitButton() {
  return (
    <button type="submit" data-cy="submit">
      保存
    </button>
  );
}


03. E2E テストの設計

テストスイート内の処理ステップ

テストスイートの最初にDB本体とテーブルを作成する。

各テストケースの最初では TRUNCATE 文でテーブル内のデータを掃除し、そのうえでテストケースを順番に実行する。

テストスイートはほかと独立しており、並列できる。

E2EテストスイートA(ほかのテストスイートとは独立)
│
├── 初回のみ
│   ├── DB本体を作成
│   └── テーブルを作成(yarn prisma migrate dev)
│
├── テストケース1
│   ├── テーブル内のデータのみを掃除(TRUNCATE文)
│   ├── 初期データを投入(dbSeed.ts)
│   └── テストを実行
│
├── テストケース2
│   ├── テーブル内のデータのみを掃除(TRUNCATE文)
│   ├── 初期データを投入(dbSeed.ts)
│   └── テストを実行
│
└── テストケース3
├── テーブル内のデータのみを掃除(TRUNCATE文)
├── 初期データを投入(dbSeed.ts)
└── テストを実行


テストスイート、テストケース

describe() 関数を使用して、テストスイートを定義できる。

テストスイートは階層的に定義でき、最大2階層まではあってもよい。

describe("<パブリックな関数名>", async () => {

  describe("<テストスイートの種別>", async () => {

    test("<テストケース名>", async () => {
    });
  });
});


テストケースの設計

▼ 正常系、異常系、境界値

正常系、異常系、境界値のテストケースをテストスイートの種別におく。

describe("<パブリックな関数名>", async () => {

  describe("<テストスイートの種別>", async () => {

    // 正常系
    test("<テストケース名>", async () => {

    });
    // 異常系
    test("<テストケース名>", async () => {

    });
    // 境界値
    test("<テストケース名>", async () => {

    });
  });
});
describe("<パブリックな関数名>", async () => {

  describe("<テストスイートの種別>", async () => {

    test("正常系:<テストケース名>", async () => {

    });
    test("異常系:<テストケース名>", async () => {

    });
    test("境界値:<テストケース名>", async () => {

    });
  });
});

▼ ファイルの命名

ファイル名パターン 用途
<リソース名>.cy.ts(単数形) 単一リソースの詳細表示・作成・編集・削除
<リソース名>s.cy.ts(複数形) 一覧表示・検索・フィルタリング

▼ テストケースの命名

test() 関数と it() 関数は、振る舞いの主語(濁した主語)としての it または test として見ることができる。

つぎのように命名すると、it または test がテストケース名の主語になって、振る舞いに着目してる感がでる。

例えば、<期待される動作> when <入力内容> で命名する。

describe("<テストスイート>", () => {
  // テストケース
  test("should return user when id is valid", () => {});
});

Cypress のドキュメントに BDD(振る舞い駆動開発)の記載がある。

Writing and organizing Cypress tests | Cypress Documentation

▼ E2Eテストの構成

import "../../support/commands";

describe("画面名", () => {
  beforeEach(() => {
    // 事前処理
    // 初期データの投入
    cy.task("resetTestDb");

    // ハイドレーションエラーの抑制
    cy.on("uncaught:exception", (error) => {
      console.log(`${error.message}`);
      return false;
    });

    // 画面操作
    // ログイン
    cy.login();


    // data-cy を使用して要素を見つけ、画面を操作する
    cy.get("[data-cy=projects]").click();

    // 閲覧系:
    // 閲覧画面に遷移 -> 閲覧操作 -> 画面結果と期待値の照合
    //
    // 登録系:
    // 登録画面に遷移 -> 登録操作 -> 登録確定/登録しない -> 画面結果と期待値の照合
    //
    // 更新系:
    // 更新画面に遷移 -> 更新操作 -> 更新確定/更新しない -> 画面結果と期待値の照合
    // 
    // 削除系:
    // 削除画面に遷移 -> 削除操作 -> 削除確定/削除しない -> 画面結果と期待値の照合
  });
});


04. 事前処理

cypress.config.ts

// 初期データの事前セットを読み込む
import seed from "fixtures/seed.json";

setupNodeEvents: (on, config) => {

  on("task", {
    dbSeed: async () => {
      const { user1, user2 } = seed.user;

      // 初期データをデータベースに挿入する
      await prisma.user.create({
        data: {
          id: user1.id,
          email: user1.email,
          name: user1.name,
        },
      });

      // 初期データをデータベースに挿入する
      await prisma.user.create({
        data: {
          id: user2.id,
          email: user2.email,
          name: user2.name,
        },
      });
    },
    // ...
  });
}


fixtures/foo/data.json

テストに使用する初期データ(例;DB 値、リクエストパラメーターなど)を設定する。

fixtures/expected/foo.json ファイル(期待値データ)の値と同じになる可能性がある。

# 初期データ
{
  "users":
    {
      "user1":
        {
          "email": "user1@gmail.com",
          "name": "testUser1",
          "password": "password",
        },
      "user2":
        {
          "email": "user2@gmail.com",
          "name": "testUser2",
          "password": "password",
        },
      ...,
    },
}


fixtures/expected/foo.json

テストに使用する期待値データを設定する。

fixtures/foo/data.json ファイル(初期データ)の値と同じになる可能性がある。

{
  "users":
    {
      "user1":
        {
          "email": "user1@gmail.com",
          "name": "testUser1",
          "password": "password",
        },
      "user2":
        {
          "email": "user2@gmail.com",
          "name": "testUser2",
          "password": "password",
        },
      ...,
    },
}


ヘルパー

Cypress.Commands.add("login", () => {
  // fixture/users/data.jsonファイルを使用する
  cy.fixture("users/data").then((seed) => {
    cy.visit("/");
    // 待機することで、Cypressがハイドレーション前に画面をロードし、エラーが発生することを避ける
    cy.wait(1000);
    // アサーションを実行し、実際値と期待値と一致するかを検証する
    cy.url().should("eq", Cypress.config().baseUrl + "/signup");

    // data-cy="signin" 要素をクリックする
    cy.get("[data-cy=signin]").click();
    // URLの実際値を取得し、期待値と一致するかを検証する
    cy.url().should("eq", Cypress.config().baseUrl + "/signin");

    // 初期データをフォームに入力する
    // データベースに挿入したユーザー情報を使用する
    cy.get("[data-cy=email]").type(seed.user.user1.email);
    cy.get("[data-cy=password]").type(seed.user.user1.password);
    // data-cy="submit" 要素をクリックし、送信する
    cy.get("[data-cy=submit]").click();
    // URLの実際値を取得し、期待値と一致するかを検証する
    cy.url().should("eq", Cypress.config().baseUrl + "/");

    // data-cy="profileEmail" 要素をクリックする
    cy.get("[data-cy=profile]").click();

    // data-cy="profileName" 要素から実際値を取得し、実際値と期待値と一致するかを検証する
    cy.get("[data-cy=profileName]").should("have.text", seed.user.user1.name);
    // data-cy="profileEmail" 要素から実際値を取得し、実際値と期待値と一致するかを検証する
    cy.get("[data-cy=profileEmail]").should("have.text", seed.user.user1.email);
  });
});


05. テストスイート

テストスイートはテストケース (テスト関数) に分類できる。

// テストスイート
describe("ユーザーの一覧を表示するテスト", () => {
  // 各テストケースに共通する処理をテスト
  beforeEach(() => {
    // 基本的な初期データをデータベースに挿入する
    cy.exec("yarn ts-node --require tsconfig-paths/register prisma/seed.ts");
    // cypress.config.ts ファイルで定義したdbSeed関数を実行する
    cy.task("dbSeed");
    // テスト用の初期データをデータベースに挿入する
    cy.exec(
      'yarn ts-node --require tsconfig-paths/register "cypress/seed/users.ts"',
    );

    // エラーハンドリングをイベントリスナーとして登録する
    cy.on("uncaught:exception", (error) => {
      console.log(`${error.message}`);
      return false;
    });

    // ヘルパーを使用してログインする
    cy.login();
    // data-cy="users" 要素をクリックする
    cy.get("[data-cy=users]").click();
  });

  // 正常系テストケース
  test("ユーザーの一覧を表示できる", () => {
    // テストケース固有の初期データ
    // fixture/expected/users.jsonファイルを使用する
    cy.fixture("users/data").then((expected) => {
      // URLの実際値を取得し、期待値と一致するかを検証する
      cy.url().should("eq", Cypress.config().baseUrl + "/users");

      // data-cy="userName" 要素から実際値を取得し、実際値と期待値と一致するかを検証する
      cy.get("[data-cy=userName]")
        .its(0)
        .should("contain.text", expected.user1.name);
      // data-cy="userEmail" 要素から実際値を取得し、実際値と期待値と一致するかを検証する
      cy.get("[data-cy=userEmail]")
        .its(0)
        .should("contain.text", expected.user1.email);
    });
  });

  // 正常系テストケース
  test("ページネーションを実行できる", () => {
    // 汎用的な初期データ
    cy.fixture("dbSeed").then((expected) => {
      // テストケース固有の初期データ
      // fixture/expected/users.jsonファイルを使用する
      cy.fixture("users/data").then((expected) => {
        const {user2} = expected.user;
        cy.wait(1000);

        cy.get("[data-cy=next]").click();
        // URLの実際値を取得し、期待値と一致するかを検証する
        cy.url().should("eq", Cypress.config().baseUrl + "/users/?page=2");
        // data-cy="previous" 要素から実際値を取得し、期待値と一致するかを検証する
        cy.get("[data-cy=userName]").its(0).should("contain.text", user2.name);

        // data-cy="previous" 要素をクリックする
        cy.get("[data-cy=previous]").click();
        // アサーションを実行し、実際値と期待値と一致するかを検証する
        cy.url().should("eq", Cypress.config().baseUrl + "/users/?page=1");

        // data-cy="userName" 要素から実際値を取得し、期待値と一致するかを検証する
        cy.get("[data-cy=userName]")
          .its(0)
          .should("contain.text", expected.user1.name);
      });
    });
  });
});