gitkadoの気まぐれ日記

島根在住エンジニアが何かに興味を持ったらブログを更新します

【フロントエンド開発のためのテスト入門】読んだ備忘録

自分用に フロントエンド開発のためのテスト入門 を読んだ備忘録を残しておきます。
以下、自分が付箋をはってた箇所をピックアップします。

備忘録

2-4 テスト戦略モデル

テストピラミッドは有名で何度も聞いたことがあった。

アイスクリームコーン型・テストピラミッド型

今回紹介されていたのはテスティングトロフィー型というものだった。

  • Testing Libraryの開発者であるKent氏が提唱するテスト戦略モデルです。
    • 効率よくテストを書くよりも、正しくソフトウェアが動作することを保証することが大切
    • ユニットテストよりもインテグレーションテストを充実させよう!!
    • 単体でなく結合させてソフトウェアを提供するので、そこの品質を補償しなければならない。
      • 問題なくソフトウェアを提供できていれば単体にバグが潜んでても問題ない(暴論)

参考: コンポーネント思考のテスト方針(ブログ)

今の現場でもユニットテストより結合テストを大切にしていたが、何故そうしているのか説明できるようになった気がします。

第5章 UIコンポーネントテスト

ここで登場したのが Webアクセシビリティ(web-a11y) です。
ブラウザの支援技術を活用してシステムを使えるのか?というものですが、人生で1度も気にしたことがありませんでした。

  • testing-library でUIテストを実装できます。
    • 基本原則で「暗黙的なロール」も含めたクエリーを優先的に使用することを推奨しています。

参考: Testing Library Queries Priority

以下ではロール"button"を指定しているが、コンポーネント側では明示的に指定していない。

// https://github.com/frontend-testing-book/unittest/tree/main/src/05/03
test("ボタンの表示", () => {
  render(<Form name="taro" />);
  expect(screen.getByRole("button")).toBeInTheDocument();
});

ユーザ操作に近いシュミレートができる testing-library/user-event が特に良さそうだった。

//  https://github.com/frontend-testing-book/unittest/blob/main/src/05/05/InputAccount.test.tsx
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { InputAccount } from "./InputAccount";

const user = userEvent.setup();

test("メールアドレス入力欄", async () => {
  render(<InputAccount />);
  // 入力欄を取得
  const textbox = screen.getByRole("textbox", { name: "メールアドレス" });
  const value = "taro.tanaka@example.com";
  // 値を入力
  await user.type(textbox, value);
  // 検証
  expect(screen.getByDisplayValue(value)).toBeInTheDocument();
});

5-7 非同期処理を含むUIコンポーネントテスト

Arrange-Act-Assert(AAA)パターン

  • 準備・実行・検証の3パターンでテストコードを構成する書き方
  • 普段自分が意識している Given-When-Then と同じだと思う

参考

test("成功時「登録しました」が表示される", async () => {
  // Arrange(準備)
  const mockFn = mockPostMyAddress();
  render(<RegisterAddress />);
  // Act(実行)
  const submitValues = await fillValuesAndSubmit();
  // Assert(検証)
  expect(mockFn).toHaveBeenCalledWith(expect.objectContaining(submitValues));
  expect(screen.getByText("登録しました")).toBeInTheDocument();
});

6-2 カバレッジレポートの読み方

カバレッジが高いからといって品質の高いテストであるとは限らない

品質をカバレッジに頼りすぎるのはNGだが、以下をパトロールするには便利な指標になる。

  • テストは足りてるけどカバレッジが低い場合
    -> 不要な実装があるのでリファクタの余地あり
  • テスト足りてなくてカバレッジが低い場合
    -> テスト観点漏れ

第7章 Webアプリケーション結合テスト

インタラクション*1も関数で抽象化することで、UIコンポーネントのそうだが直感的に理解できる可読性の高いテストコードが書ける。

// https://github.com/frontend-testing-book/nextjs/blob/main/src/components/templates/MyPosts/Posts/Header/index.test.tsx
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import mockRouter from "next-router-mock";
import { Header } from "./";

const user = userEvent.setup();

function setup(url = "/my/posts?page=1") {
  mockRouter.setCurrentUrl(url);
  render(<Header />);
  const combobox = screen.getByRole("combobox", { name: "公開ステータス" });
  // インタラクション関数
  async function selectOption(label: string) {
    await user.selectOptions(combobox, label);
  }
  return { combobox, selectOption };
}

test("公開ステータスを変更すると、status が変わる", async () => {
  const { selectOption } = setup();
  expect(mockRouter).toMatchObject({ query: { page: "1" } });
  await selectOption("公開");
  expect(mockRouter).toMatchObject({
    query: { page: "1", status: "public" },
  });
  await selectOption("下書き");
  expect(mockRouter).toMatchObject({
    query: { page: "1", status: "private" },
  });
});

第8章 UIコンポーネントエクスプローラ

Storybook

誤解

実際

  • 1コンポーネントでも条件や振る舞いを加味したら複数のStoryが存在する
  • 細かいコンポーネントの単位で実装/テストが可能になる
    • Pageに組み込まなくてもコンポーネント単体で確認ができるのは知らなかった
  • AtomicDesignの粒度が人によってマチマチになってる現場とかでは粒度を揃えるきっかけになりそう
  • 運用コストが気になるところだがテストで利用できるのでプラマイゼロくらいな印象
    • 以下が実装例だが、Givenに当たる部分をStoryで賄える
// https://github.com/frontend-testing-book/nextjs/blob/main/src/components/organisms/AlertDialog/index.test.tsx
import { composeStories } from "@storybook/testing-react";
import { render, screen } from "@testing-library/react";
import * as stories from "./index.stories";

const { Default, CustomButtonLabel, ExcludeCancel } = composeStories(stories);

describe("AlertDialog", () => {
  test("Default", () => {
    render(<Default />);
    expect(screen.getByRole("alertdialog")).toBeInTheDocument();
  });

  test("CustomButtonLabel", () => {
    render(<CustomButtonLabel />);
    expect(screen.getByRole("button", { name: "OK" })).toBeInTheDocument();
    expect(
      screen.getByRole("button", { name: "キャンセル" })
    ).toBeInTheDocument();
  });

  test("ExcludeCancel", () => {
    render(<ExcludeCancel />);
    expect(screen.getByRole("button", { name: "OK" })).toBeInTheDocument();
    expect(
      screen.queryByRole("button", { name: "CANCEL" })
    ).not.toBeInTheDocument();
  });
});

気になること

  • デザイナと共有できるというのはあまり納得いってない
    • 開発環境で見れるものという認識だからレビューで見せるくらいしか使えなさそう
  • コンポーネントフレームワーク(Vuetifyなど)使ってたら活躍の場が少ない?

第9章 ビジュアルリグレッションテスト

ビジュアルテスト

  • Jestでは検証できないブラウザを使った見え方のテスト
  • E2Eもブラウザを使ってるが目的が違う

実現方法

  • Storybookがあるとビジュアルテストを追加作業ほぼ無しで実現できる
    • storycapでStoryごとのキャプチャを取得
    • reg-cliで修正前後のキャプチャを比較してデグレを検知

著者のオンラインイベント

codezine.connpass.com

そこで聞いた耳寄り情報

作業メモ

とりまNuxt3のアプリケーションにStorybookをインストールしておきました。

$ npx sb init --type vue3 --builder vite
$ yarn storybook

*1:何らかのユーザ操作をトリガーにシステムがそれに応じた反応を起こすこと