単体テスト作成
cf.
事前準備
- Vitestをインストールします:
npm install -D vitest
package.jsonにtestスクリプトを追加します:
{
"scripts": {
"test": "vitest"
}
}
- Testing Library向けのパッケージをインストールします:
npm install --save-dev @testing-library/react @testing-library/user-event
- Vites向けのパッケージをインストールします:
npm install --save-dev \
@testing-library/svelte \
@testing-library/jest-dom \
@sveltejs/vite-plugin-svelte \
vitest \
jsdom
vitest-setup.jsを下記内容で作成します:
import "@testing-library/jest-dom/vitest";
vite.config.tsを下記内容で更新します:
import { svelte } from "@sveltejs/vite-plugin-svelte";
import { svelteTesting } from "@testing-library/svelte/vite";
import { defineConfig } from "vitest/config";
export default defineConfig({
plugins: [svelte(), svelteTesting()],
test: {
environment: "jsdom",
setupFiles: ["./vitest-setup.js"],
},
});
src/test-util.tsxを作成します:
import { render, type RenderResult } from "@testing-library/react";
import { Provider } from "react-redux";
import { store } from "./app/store";
/**
* テスト対象のUIについてRedux storeを付与してレンダリングします。
*
* @param ui テスト対象コンポーネント
* @returns Redux store付きレンダリング
*/
export const renderWithProvider = (ui: React.ReactElement): RenderResult => {
return render(<Provider store={store}>{ui}</Provider>);
};
テスト作成
Counter.test.tsx
Increment, Decrementボタンを持つコンポーネントについて、ボタン操作を行なってカウントが更新されることをテストします:
import "@testing-library/jest-dom/vitest";
import { screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it } from "vitest";
import { renderWithProvider } from "../../test-utils";
import { Counter } from "./Counter";
describe("Counter component", () => {
it("Increments and decrements the count", async () => {
renderWithProvider(<Counter title="Unit test" />);
expect(screen.getByText("Unit test")).toBeInTheDocument();
const incrementButton = screen.getByText("Increment");
const decrementButton = screen.getByText("Decrement");
const countDisplay = screen.getByText("0");
await userEvent.click(incrementButton);
expect(countDisplay).toHaveTextContent("1");
await userEvent.click(incrementButton);
expect(countDisplay).toHaveTextContent("2");
await userEvent.click(decrementButton);
expect(countDisplay).toHaveTextContent("1");
});
});
Auth.test.tsx
ユーザ名およびパスワードを入力してログインAPIを呼び出すコンポーネントについて、ログイン成否によって画面上のメッセージが適切に表示されるかテストします。authApiクラスに実装したloginApi関数をモック化しています:
import { cleanup, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { renderWithProvider } from "../../test-utils";
import { Auth } from "./Auth";
import * as authApi from "./authApi";
describe("Auth component", () => {
let u: ReturnType<typeof userEvent.setup>;
beforeEach(() => {
u = userEvent.setup();
});
afterEach(() => {
vi.restoreAllMocks();
cleanup();
});
it("Login success", async () => {
vi.spyOn(authApi, "login").mockImplementation(async (req) => {
if (req.name !== "nob" || req.password !== "passwd") {
throw new Error("テスト失敗"); // 入力値が関数に正しく渡っていなければテスト失敗
}
return Promise.resolve({ ok: true, valid: true });
});
renderWithProvider(<Auth />);
const nameInput = screen.getByLabelText("name");
const passwordInput = screen.getByLabelText("password");
await u.type(nameInput, "nob");
await u.type(passwordInput, "passwd");
await u.click(screen.getByText("ログイン"));
expect(screen.getByText("ログイン完了")).toBeInTheDocument();
});
it("Login failed", async () => {
vi.spyOn(authApi, "login").mockImplementation(async (req) => {
if (req.name !== "nob" || req.password !== "passwd") {
throw new Error("テスト失敗"); // 入力値が関数に正しく渡っていなければテスト失敗
}
return Promise.resolve({ ok: false, message: "単体テスト上のエラー" });
});
renderWithProvider(<Auth />);
const nameInput = screen.getByLabelText("name");
const passwordInput = screen.getByLabelText("password");
await u.type(nameInput, "nob");
await u.type(passwordInput, "passwd");
await u.click(screen.getByText("ログイン"));
expect(screen.getByText("単体テスト上のエラー")).toBeInTheDocument();
});
it("Fail to call API", async () => {
vi.spyOn(authApi, "login").mockImplementation(async () => {
throw new Error("テスト失敗"); // APIとの疎通失敗想定
});
renderWithProvider(<Auth />);
const nameInput = screen.getByLabelText("name");
const passwordInput = screen.getByLabelText("password");
await u.type(nameInput, "nob");
await u.type(passwordInput, "passwd");
await u.click(screen.getByText("ログイン"));
expect(
screen.getByText("不明なエラーが発生しました。"),
).toBeInTheDocument();
});
});
テスト起動
- 下記コマンドでプロジェクト内のテストを一括実行します:
npm run test
- ディレクトリを指定することで特定のコンポーネントのみテストできます:
npm run test src/features/{component}
カバレッジ出力
cf. https://vitest.dev/guide/coverage.html
- パッケージをインストールします:
npm i -D @vitest/coverage-v8
vite.config.tsにtest.coverageを追加します:
test: {
coverage: {
provider: "v8",
include: ["src/features"], // features配下のみカバレッジ集計
exclude: ["src/features/**/*Api*.ts", "src/features/**/*module.scss"] // xxxApi.tsはモック化するのでカバレッジ対象外、scssも対象外
}
}
package.jsonにカバレッジ出力スクリプトを追加します:
{
"scripts": {
"coverage": "vitest run --coverage"
}
}
- 下記コマンドを実行すると
coverageディレクトリ配下にカバレッジレポートが出力されます:
npm run coverage