5
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

フロントエンドのテスト入門

Last updated at Posted at 2024-06-30

Vitestを使ったフロントエンドのテストを試してみます

準備

Reactのインストール

terminal
 //インストール開始
 npm create vite@latest
 
 //プロジェクト名入力
 Select a framework: » React
 Select a variant: » TypeScript + SWC
 
 //移動
 cd プロジェクト名
 
 //パッケージインストール
 npm i
 
 //起動
 npm run dev

表示に従って http://localhost:5173/ を開きます
Vite + Reactの画面が立ち上がればokです

ビルドする場合

terminal
 //ビルド(distディレクトリに静的に出力)
 npm run build
 
 //ビルドしたものをプレビューする(ビルドでdistに出力したものを立ち上げる)http://localhost:5173/
 npm run preview 

不要ファイル削除

  • src/App.css
  • src/index.css

コード編集

src/App.tsx
export const App = () => {
  return <>ddd</>;
};
src/main.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import { App } from "./App";


ReactDOM.createRoot(document.getElementById("root")!).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

Vitestのインストール

terminal
 npm i -D vitest happy-dom @vitest/coverage-v8 @testing-library/react @testing-library/user-event @testing-library/jest-dom @types/jest
パッケージ 内容
vitest Jest互換のテストフレームワークで、ネイティブ ESM サポートしているのでトランスパイル不要
happy-dom jsdomより速い、Node.js用の多くのWeb標準を実装したJavaScriptライブラリ
@vitest/coverage-v8 Vitestのカバレッジプロバイダー
@testing-library/react Reactのテストコード作成のためのツール
@testing-library/user-event ユーザーがブラウザを操作したときにブラウザで発生する実際のイベントをシミュレートする
@testing-library/jest-dom Testing Libraryといっしょに使う、Jest用のDOM要素のマッチャーを提供するライブラリ
@types/jest jestの型定義

package.jsonへの追記

package.json
   "scripts": {
     "test": "vitest",
     "test:watch": "vitest watch",
     "coverage": "vitest run --coverage"
   },

ファイルの新規作成

vitest.setup.ts
import "@testing-library/jest-dom/vitest";
App.tsx
import "@testing-library/jest-dom";
export const App = () => {
  return <></>;
};
vite.config.ts
 /// <reference types="vitest"/>
 import { defineConfig } from 'vite'
 import react from '@vitejs/plugin-react-swc'
 
 // https://vitejs.dev/config/
 export default defineConfig({
   plugins: [react()],
   test: {
     globals: true,
     environment: "happy-dom",
     setupFiles: ["./vitest.setup.ts"]
   }
 })

テスト

足し算の検証

2つの数字を足す関数を作り、テストしてみます

関数

src/calcs/Sum/Sum.ts
export const addition = (firstNumber: number, secondNumber: number): number => {
    return firstNumber + secondNumber;
  };
 
export const  division = (dividend: number, divisor: number): number => {
    return dividend / divisor;
  }

テスト

src/calcs/Sum/Sum.test.ts
import { addition, division } from "./Sum";

test("1+2が3になる", () => {
  expect(addition(1, 2)).toBe(3);
});

test("10/2が5になる", () => {
  expect(division(10, 2)).toBe(5);
});

※testではなくitと書くことも可能です

テスト実行

terminal
 npm run test src/calcs/Sum/Sum.test.ts
 
 Test Files  1 passed (1)
      Tests  2 passed (2)

成功しましたが、本当に検証できているか確かめるためにわざと失敗してみます

src/calc/Sum/Sum.test.ts
test("1+3が3になる", () => {
  expect(sum(1, 3)).toBe(3);
})

テスト実行

terminal
npm run test src/calc/Sum/Sum.test.ts

Test Files  1 failed (1)
     Tests  2 failed (2)

ちゃんと失敗しました。

マッチャーの紹介

なんの説明もせずに「toBe」とか使ってしまいましたが、このようなものをマッチャーと呼びます

等しいことを検証するマッチャー 説明
toEqual 等しい(number, string)
toBe 等しい(number, stringobject,) ※インタンスの参照まで確認する
数値の大小を検証するマッチャー 説明
toBeGreaterThan より大きい
toBeGreaterThanOrEqual 以上
toBeLessThan 未満
toBeLessThanOrEqual 以下
toEqual 等しい
文字列を検証するマッチャー 説明
toContain 部分一致
toMatch 正規表現
toHaveLength 文字列の長さ
stringContaining オブジェクトに含まれる文字列の検証(文字列で指定)
stringMatching オブジェクトに含まれる文字列の検証(正規表現で指定)
真偽値を検証するマッチャー 説明
toBeTruthy 真であることを検証
toBeFalthy 偽であることを検証(null、undefinedはtoBeFalthyに一致するが、厳密に検証するならtoBeNull、toBeUndefinedを使う)
配列を検証するマッチャー 説明
toContain 配列に特定のプリミティブが含まれているかの検証
toHaveLength 配列の要素数の検証
toContainEqual 配列に特定のオブジェクトが含まれているかの検証
arrayContaining 配列に特定のオブジェクトが含まれているかの検証(引数に与えた配列要素が全て含まれているとテスト成功)
オブジェクトを検証するマッチャー
toMatchObject プロパティが部分的に一致していればテストは成功する(一致しないプロパティがある場合は失敗)
toHaveProperty 特定のプロパティが存在するかの検証
objectContaining オブジェクトに含まれるオブジェクトの検証(対象のプロパティが期待値のオブジェクトと部分的に一致すればテスト成功
null、undefined、NaNを検証するマッチャー 説明
toBeNull nullであるかの検証
toBeUndefined undefinedであるかの検証
toBeDefined 定義されているか(undefinedでないか)の検証
toBeNaN NaNであるかの検証
例外処理の検証 説明
toThrow 関数が例外をスローするの検証(例外の種類はノーチェック)
toThrowError 関数が例外をスローするの検証(例外の種類もチェック)

マッチャーの前にnotをつけると判定を反転できます

ts
not.マッチャー

例外処理(エラー)の検証

3種類の例外処理の検証をしてみます

  • JavaScript の最大整数値は Number. MAX_SAFE_INTEGER という定数で定義されており、その値は 9007199254740991
  • JavaScript の最小整数値は Number. MIN_SAFE_INTEGER という定数で定義されており、その値は -9007199254740991
  • 0で割ることはできない
src/calcs/MinMax/MinMax.ts
export const addition = (firstNumber: number, secondNumber: number): number | Error => {
  const result = firstNumber + secondNumber;
  if (result > Number.MAX_SAFE_INTEGER) {
    throw new Error("計算結果がJavaScriptで扱うには大きすぎます")
  }
  if (result < Number.MIN_SAFE_INTEGER) {
    throw new Error("計算結果がJavaScriptで扱うには小さすぎます")
  }
  return result;
};
export const division = (dividend: number, divisor: number): number | Error => {
  if (divisor === 0) {
    throw new Error("0で割ることはできません")
  }
  return dividend / divisor;
}
src/calcs/MinMax/MinMax.test.ts
import { addition, division } from "./MinMax";
 
describe("足し算のテスト", () => {
  test("1+2が3になる", () => {
    expect(addition(1, 2)).toBe(3);
  });
 
  test("計算結果がJavaScriptで扱うには小さすぎる場合にエラーを出す", () => {
    expect(() => addition(-9007199254740990, -2)).toThrow("計算結果がJavaScriptで扱うには小さすぎます");
  });
  test("計算結果がJavaScriptで扱うには大きすぎる場合にエラーを出す", () => {
    expect(() => addition(9007199254740990, 2)).toThrow("計算結果がJavaScriptで扱うには大きすぎます");
  });
 });
 
describe("割り算のテスト", () => {
  test("10/2が5になる", () => {
    expect(division(10, 2)).toBe(5);
  });
 
  test("10/2が5になる", () => {
    expect(() => division(10, 0)).toThrow("0で割ることはできません");
  });
});

※テストはdescribe関数でグルーピング可能です
(ネストも可能)

テスト実行

terminal
npm test src/calcs/MinMax/MinMax.test.ts
 
Test Files  1 passed (1)
     Tests  5 passed (5)

button要素の検証

src/components/TestButton/TestButton.tsx
import { FC } from "react";

type testButtonProps = {
  label: string;
  onClick: (event: React.MouseEvent<HTMLButtonElement>) => void;
  disabled: boolean;
  type: "button" | "reset" | "submit";
};

export const TestButton: FC<testButtonProps> = ({
  label,
  onClick,
  disabled,
}) => {
  return (
    <div>
      <button type='button' onClick={onClick} disabled={disabled}>
        {label}
      </button>
    </div>
  );
};
src/components/TestButton/TestButton.test.tsx
import { render, screen } from "@testing-library/react";
import "@testing-library/jest-dom/vitest";
import { TestButton } from "./TestButton";

test("ボタンが表示されている", () => {
  const handleButtonClick = (event: React.MouseEvent<HTMLButtonElement>) => {
    alert(`${event.currentTarget.type}を押しました。`);
  };
  
  render(
    <TestButton
      label='Yes'
      type={"button"}
      onClick={handleButtonClick}
      disabled={false}
    />
  );
  const testElement = screen.getByRole("button");
  expect(testElement).toBeInTheDocument();
});
 
test("ボタンのラベルの文字が「Yes」", () => {
  const handleButtonClick = (event: React.MouseEvent<HTMLButtonElement>) => {
    alert(`${event.currentTarget.type}を押しました。`);
  };
 
  render(
    <TestButton
      label='Yes'
      type={"button"}
      onClick={handleButtonClick}
      disabled={false}
    />
  );
  const testElement2 = screen.getByText("Yes");
  expect(testElement2).toBeInTheDocument();
});
 
test("2つのボタンが表示されている", () => {
  const handleButtonClick = (event: React.MouseEvent<HTMLButtonElement>) => {
    const buttonElement = event.target as HTMLButtonElement;
    const buttonText = buttonElement.innerText;
    alert(`${buttonText}ボタンが押されました`);
  };

  render(
    <>
      <TestButton
        label='Yes'
        type={"button"}
        onClick={handleButtonClick}
        disabled={false}
      />
      <TestButton
        label='No'
        type={"button"}
        onClick={handleButtonClick}
        disabled={false}
     />
  </>
);
 
  //buttonロールで、アクセシブルネームが「Yes」のものを変数testElement1に入れる
  const testElement1 = screen.getByRole("button", { name: "Yes" });
  //buttonロールで、アクセシブルネームが「No」のものを変数testElement2に入れる
  const testElement2 = screen.getByRole("button", { name: "No" });
 
  //testElement1とtestElement2がドキュメント内にあることを検証する
  expect(testElement1 && testElement2).toBeInTheDocument();
});
 
test("ボタンのラベルの文字が「Yes」と「No」", () => {
  const handleButtonClick = (event: React.MouseEvent<HTMLButtonElement>) => {
    alert(`${event.currentTarget.type}を押しました。`);
   };
 
  render(
    <>
      <TestButton
        label='Yes'
        type={"button"}
        onClick={handleButtonClick}
        disabled={false}
      />
      <TestButton
        label='No'
        type={"button"}
        onClick={handleButtonClick}
        disabled={false}
      />
    </>
  );
 
  //テキストが「Yes」の要素を変数testElement1に入れる
  const testElement1 = screen.getByText("Yes");
  //テキストが「Yes」の要素を変数testElement1に入れる
  const testElement2 = screen.getByText("No");
 
  //testElement1とtestElement2がドキュメント内にあることを検証する
  expect(testElement1 && testElement2).toBeInTheDocument();
});

input要素(type=text)の検証

コンポーネント

src/components/TestTextbox/TestTextbox.tsx
import { FC } from "react";
 
type testTextboxProps = {
  labelword: string;
  pairword: string;
  type: "text" | "password" | "search" | "email" | "url" | "number";
};

export const Textbox: FC<testTextboxProps> = ({
  labelword,
  pairword,
  type,
}) => {
  return (
    <>
      <label htmlFor={pairword}>{labelword}</label>
      <input type={type} id={pairword} />
    </>
  );
};

テストコード

src/components/TestTextbox/TestTextbox.test.tsx
import { render, screen } from "@testing-library/react";
import "@testing-library/jest-dom/vitest";
import { Textbox } from "./TestTextbox";

test("テキストボックスが表示されている1", () => {
  //Textboxコンポーネントをレンダリングする
  render(<Textbox labelword='名前' pairword='aaa' type='text' />);
 
  //ロールがtextboxの要素を取得して変数textboxElementにいれる
  const textboxElement = screen.getByRole("textbox");
 
  //ドキュメント内にtextboxElementが存在することを検証
  expect(textboxElement).toBeInTheDocument();
});

test("テキストボックスが表示されている2", () => {
  //Textboxコンポーネントをレンダリングする
  render(<Textbox labelword='名前' pairword='aaa' type='text' />);
  screen.debug();
  //ロールがtextboxの要素を取得して変数textboxElementにいれる
  const textboxElement = screen.getByLabelText("名前");
 
  //ドキュメント内にtextboxElementが存在することを検証
  expect(textboxElement).toBeInTheDocument();
});

テスト実行

terminal
npm run test src/components/TestTextbox/TestTextbox.test.tsx

Test Files  1 passed (1)
     Tests  1 passed (1)

Heading要素の検証

コンポーネント1

src/components/TestHeading/TestHeading1.tsx
import { FC } from "react";

type testHeadingProps = {
  text: string;
};

export const TestHeading1: FC<testHeadingProps> = ({ text }) => {
  return (
    <>
      <h1>{text}</h1>
    </>
  );
};

コンポーネント2

src/components/TestHeading/TestHeading2.tsx
import { FC } from "react";

type testHeadingProps = {
  text: string;
};

export const TestHeading2: FC<testHeadingProps> = ({ text }) => {
  return (
    <>
      <h2>{text}</h2>
    </>
  );
};

テストコード

src/components/TestHeading/TestHeading.test.tsx
import { render, screen } from "@testing-library/react";
import "@testing-library/jest-dom/vitest";
import { TestHeading1 } from "./TestHeading1";
import { TestHeading2 } from "./TestHeading2";

test("h1要素が表示されている", () => {
  render(<TestHeading1 text='rrrrrrrrrrrr' />);
  const testElement = screen.getByRole("heading");
  expect(testElement).toBeInTheDocument();
});
 
test("h2要素が表示されている", () => {
  render(<TestHeading2 text='rrrrrrrrrrrr' />);
  const testElement = screen.getByRole("heading");
  expect(testElement).toBeInTheDocument();
});
 
test("h1要素が表示されている(h1とh2が表示されている状態でh1を選別)", () => {
  render(
    <>
      <TestHeading1 text='rrrrrrrrrrrr' />
      <TestHeading2 text='rrrrrrrrrrrr' />
    </>
  );
  const testElement = screen.getByRole("heading", { level: 1 });
  expect(testElement).toBeInTheDocument();
});

テスト実行

terminal
npm run test src/components/TestHeading/TestHeading.test.tsx

Test Files  1 passed (1)
     Tests  3 passed (3)

DOM関連のマッチャー

DOM関連のマッチャー 説明
toBeDisabled disabledであることを検証
toBeEnabled disabledでないことを検証
toBeEmptyDOMElement DOMの中身が空であることを検証
toBeInTheDocument DOMの中身が空でないことを検証
toBeInvalid input要素などがinvalidか検証
toBeRequired input要素などrequiresか検証
toBeValid input要素などがinvalidではないか検証
toBeVisible 要素が目に見える状態か検証
toContainElement 要素を子要素として持っているか検証
toContainHTML 要素を子要素として持っているか検証
toHaveClass 対象のクラスを持っているか検証
toHaveFocus focus状態であることを検証
toHaveFormValues formの値を検証
toHaveStyle 対象のstyleを検証
toHaveTextContent 中の文字要素を検証
toHaveValue inputなどのvalueを検証
toBeChecked checkedであることを検証

WAI-ARIAのrole属性一覧

HTML要素 暗黙のロール 備考
<article> artilcle
<aside> complementary
<nav> navigation
<header> banner
<footer> contentinfo
<main> main
<section> region aria-labelledbyが指定されている場合のみ
<form> form アクセシブルネームを持つ場合のみ
<button> button
<a href=xxxxx"> link href属性を持つ場合のみ
<input type="checkbox"> checkbox
<input type="radio"> radio
<input type="button"> button
<input type="text"> textbox
<input type="password"> なし
<input type="search"> searchbox
<input type="email"> textbox
<input type="url"> textbox
<input type="tel"> textbox
<input type="number"> spinbutton
<input type="range"> slider
<select> listbox
<optgroup> group
<option> option
<ul> list
<li> list
<table> table
<caption> caption
<th> columnheader/rowheader 列ヘッダーか、行ヘッダーかによる
<td> cell
<tr> row
<fieldset> group
<legend> なし
<h1> heading
<h2> heading
<h3> heading
<h4> heading
<h5> heading
<h6> heading

クエリ

クエリのおおまかな分類

種類 説明
getBy 見つからなければエラーが返される
queryBy 見つからなければnullが返される
findBy 非同期関数に使用することができる

※~ByAll 対象の要素を全て取得することができる

getByの優先順位

分類 優先順位 getBy 説明
誰でもアクセスできるクエリ 1 getByRole ロール(役割)を判断する
誰でもアクセスできるクエリ 2 getByLabelText ラベルだけ(役割は判断しない)
誰でもアクセスできるクエリ 3 getByPlaceholderText
誰でもアクセスできるクエリ 4 getByText <div><span><p>など
誰でもアクセスできるクエリ 5 getByDisplayValue
セマンティッククエリ 6 getByAltText 要素のalt属性
セマンティッククエリ 7 getByTitle
テストID 8 getByTestId

テストをしやすくするツール

テストのデバッグ

その時点のHTMLを確認できます

tsx
screen.debug()

Roleを出力する

下記を表示できます

  • roleごとの要素一覧
  • nameの値
tsx
 import { logRoles } from '@testing-library/dom'

logRoles(screen.getByRole('ロール名'))

testing playground

Chrome拡張をインストールする必要があります

ブラウザに表示する必要があるため、App.tsxに調べたいコンポーネントを配置してからサーバーを起動します

terminal
npm run dev

1.Chromeのdevツールの「Element」「console」の並びの一番右の「>>」をクリック
2.「Testing Playground」を選択
3.左上の「select element」のアイコンをクリックしてから、ページの要素をホバーするとクエリの候補が確認できる(suggested queryの部分からコピーもできる)

5
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?