🧪

Nuxt3 × 単体テスト × E2Eテスト実装例

2023/02/20に公開

はじめに

この記事は、単体テストやE2Eテストを自動化させるにあたって
整理した要点や実装例を記載したものである。
この記事の対象は、テスト自動化の初心者に向けたものである。

環境(一部抜粋)

  • Nuxt3
  • Vitest
  • Playwright

単体テストの要点

  • テスト名は日本語で開発者以外が理解できるものにすること
  • テスト名は3つのレベルを含めること
sample.test.ts
describe('ドメイン', () => {
  describe('ユースケースやシナリオ', () => {
    test('期待する結果', () => {
    })
  })
})
  • テストケースは、AAAパターンに従うこと
sample.test.ts
it('期待する結果', () => {
  // 準備 Arrange
    
  // 実行 Act

  // 確認 Assert
})
  • 内部実装の詳細をテストしないこと
  • テスト対象は、各ドメインモデルとすること
    また、一部複雑なロジックのクラスや関数は例外的に単体テストを行うこと
    コントローラーやレポジトリの単体テストはせず、E2Eテストで間接的にテストを行うこと

Vitestによる単体テストの実装例

npm run testでテストを実行する想定

package.json
{
  "scripts": {
    "test": "vitest"
  }
}
vitest.config.ts
import path from "path";
import { defineConfig, configDefaults } from "vitest/config";

export default defineConfig({
  test: {
    // "e2e/*"はE2Eテスト用のフォルダとして使用するためテスト対象から除外する
    exclude: [...configDefaults.exclude, "e2e/*"],
    // テストファイルのインポート文でパス解決が必要な場合は、ここを修正する
    alias: {
      "~~": path.resolve(__dirname),
    },
  },
});
tests/useEntity.test.ts
import { describe, test, expect } from "vitest";
import { UserEntity } from "../server/domain/user/userEntity";

// ユーザードメインの機能要件をテストする
describe("ユーザー", () => {
  describe("ユーザーを登録する", () => {
    test("ユーザー登録できること", () => {
      // 準備
      const testData = {
        uid: "random-uid",
        name: "田中太郎",
        mailAddress: "sample@gmail.com",
      };

      // 実行
      const user = UserEntity.create(testData);

      // 確認
      expect(user.data?.value).toEqual({ ...testData, status: "active" });
    });

    test("ユーザー登録する場合、uidは必須であること", () => {
      // 準備
      const testData = {
        uid: "",
        name: "田中太郎",
        mailAddress: "sample@gmail.com",
      };

      // 実行
      const user = UserEntity.create(testData);

      // 確認
      expect(user.error?.message).toBe("UIDは必須です。");
    });

    test("ユーザー登録する場合、ユーザー名は必須であること", () => {
      // 準備
      const testData = {
        uid: "random-id",
        name: "",
        mailAddress: "sample@gmail.com",
      };

      // 実行
      const user = UserEntity.create(testData);

      // 確認
      expect(user.error?.message).toBe("ユーザー名は必須です。");
    });

    test("ユーザー登録する場合、メールアドレスは必須であること", () => {
      // 準備
      const testData = {
        uid: "random-id",
        name: "田中太郎",
        mailAddress: "",
      };

      // 実行
      const user = UserEntity.create(testData);

      // 確認
      expect(user.error?.message).toBe("メールアドレスは必須です。");
    });
  });
});

E2Eテストの要点

  • テスト名は日本語で開発者以外が理解できるものにする
  • テスト名は3つのレベルを含めること
sample.test.ts
// 例
test.describe("画面", async () => {
  test.describe("ふるまい", async () => {
    test("期待する結果", async ({ page }) => {
    });
  });
});
  • Page Object Modelsパターンに従うこと
  • 原則的に1テストケースあたり複数確認フェーズOKとする
  • 原則的にロケーターは文言から取得するが、例外的にdata-testidも許容する

PlaywrightによるE2Eテストの実装例

npm run e2eでE2Eテストを実行する想定

package.json
{
  "scripts": {
    "dev": "dotenv -e .env.development -- nuxi dev",
    "e2e": "npx playwright test"
  }
}
playwright.config.ts
import { defineConfig, devices } from "@playwright/test";

export default defineConfig({
  // テストフォルダ格納先パス指定
  testDir: "./e2e",
  timeout: 30 * 1000,
  expect: {
    timeout: 5000,
  },
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: "html",
  use: {
    actionTimeout: 0,
    baseURL: "http://localhost:3000",
    trace: "on-first-retry",
  },
  // Chromeブラウザのみでテストを確認する
  projects: [
    {
      name: "chromium",
      use: { ...devices["Desktop Chrome"] },
    },

    // {
    //   name: "firefox",
    //   use: { ...devices["Desktop Firefox"] },
    // },

    // {
    //   name: "webkit",
    //   use: { ...devices["Desktop Safari"] },
    // },

    /* Test against mobile viewports. */
    // {
    //   name: 'Mobile Chrome',
    //   use: { ...devices['Pixel 5'] },
    // },
    // {
    //   name: 'Mobile Safari',
    //   use: { ...devices['iPhone 12'] },
    // },

    /* Test against branded browsers. */
    // {
    //   name: 'Microsoft Edge',
    //   use: { channel: 'msedge' },
    // },
    // {
    //   name: 'Google Chrome',
    //   use: { channel: 'chrome' },
    // },
  ],
  // outputDir: 'test-results/',
  // テスト実行時、Nuxt3開発サーバーを事前に起動する
  webServer: {
    command: "npm run dev",
    port: 3000,
  },
});
e2e/lndexPage.test.ts
import { test, expect } from "@playwright/test";

test.describe("ログイン画面", async () => {
  test("初期表示", async ({ page }) => {
    await page.goto("/");
    await page.getByText("App").waitFor();
    await expect(page).toHaveTitle(/ログイン/);
    await expect(page.getByRole("heading", { name: "App" })).toBeVisible();
    await expect(page.getByLabel("メールアドレス")).toBeVisible();
    await expect(page.getByLabel("パスワード")).toBeVisible();
    await expect(page.getByRole("button", { name: "ログイン" })).toBeVisible();
    await expect(
      page.getByRole("link", { name: "または、新規登録はこちら" })
    ).toBeVisible();
  });
});

参考

https://vitest.dev/guide/
https://playwright.dev/docs/intro

Discussion