Vitestを使ったフロントエンドのテストを試してみます
準備
Reactのインストール
//インストール開始
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です
ビルドする場合
//ビルド(distディレクトリに静的に出力)
npm run build
//ビルドしたものをプレビューする(ビルドでdistに出力したものを立ち上げる)http://localhost:5173/
npm run preview
不要ファイル削除
- src/App.css
- src/index.css
コード編集
export const App = () => {
return <>ddd</>;
};
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のインストール
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への追記
"scripts": {
"test": "vitest",
"test:watch": "vitest watch",
"coverage": "vitest run --coverage"
},
ファイルの新規作成
import "@testing-library/jest-dom/vitest";
import "@testing-library/jest-dom";
export const App = () => {
return <></>;
};
/// <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つの数字を足す関数を作り、テストしてみます
関数
export const addition = (firstNumber: number, secondNumber: number): number => {
return firstNumber + secondNumber;
};
export const division = (dividend: number, divisor: number): number => {
return dividend / divisor;
}
テスト
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と書くことも可能です
テスト実行
npm run test src/calcs/Sum/Sum.test.ts
Test Files 1 passed (1)
Tests 2 passed (2)
成功しましたが、本当に検証できているか確かめるためにわざと失敗してみます
test("1+3が3になる", () => {
expect(sum(1, 3)).toBe(3);
})
テスト実行
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をつけると判定を反転できます
not.マッチャー
例外処理(エラー)の検証
3種類の例外処理の検証をしてみます
- JavaScript の最大整数値は Number. MAX_SAFE_INTEGER という定数で定義されており、その値は 9007199254740991
- JavaScript の最小整数値は Number. MIN_SAFE_INTEGER という定数で定義されており、その値は -9007199254740991
- 0で割ることはできない
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;
}
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関数でグルーピング可能です
(ネストも可能)
テスト実行
npm test src/calcs/MinMax/MinMax.test.ts
Test Files 1 passed (1)
Tests 5 passed (5)
button要素の検証
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>
);
};
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)の検証
コンポーネント
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} />
</>
);
};
テストコード
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();
});
テスト実行
npm run test src/components/TestTextbox/TestTextbox.test.tsx
Test Files 1 passed (1)
Tests 1 passed (1)
Heading要素の検証
コンポーネント1
import { FC } from "react";
type testHeadingProps = {
text: string;
};
export const TestHeading1: FC<testHeadingProps> = ({ text }) => {
return (
<>
<h1>{text}</h1>
</>
);
};
コンポーネント2
import { FC } from "react";
type testHeadingProps = {
text: string;
};
export const TestHeading2: FC<testHeadingProps> = ({ text }) => {
return (
<>
<h2>{text}</h2>
</>
);
};
テストコード
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();
});
テスト実行
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を確認できます
screen.debug()
Roleを出力する
下記を表示できます
- roleごとの要素一覧
- nameの値
import { logRoles } from '@testing-library/dom'
logRoles(screen.getByRole('ロール名'))
testing playground
Chrome拡張をインストールする必要があります
ブラウザに表示する必要があるため、App.tsxに調べたいコンポーネントを配置してからサーバーを起動します
npm run dev
1.Chromeのdevツールの「Element」「console」の並びの一番右の「>>」をクリック
2.「Testing Playground」を選択
3.左上の「select element」のアイコンをクリックしてから、ページの要素をホバーするとクエリの候補が確認できる(suggested queryの部分からコピーもできる)