Remixコンポーネントをテストする方法
変更履歴
変更日 | 変更内容 |
---|---|
2024/04/05 | パッケージのアップデートと GitHub Actions を設定 |
2024/04/06 | useOutletContext のテストを追加 |
2024/05/09 | loader で remix-auth の isAuthenticated を行なっている場合のテストを追加 |
はじめに
みなさん、Remix のテストはどのように書いていますでしょうか?
testing-library
で画面の表示やボタンのテストを書いたり、vitest
やjest
でテストを書いたりしていると思います。
ただ、私はこれまでは Remix の loader
や action
はどのように書いてよいかわかりませんでした。
今回、公式 HP を読んでいたら、@remix-run/testing
があったので、こちらを元にテストを書いていきます!
リポジトリはこちら
テストのリポジトリを作ってみました!
よければご参照ください。
@remix-run/testing をインストールする
以下のコマンドを入力して@remix-run/testing
をインストールします。
npm install --save-dev @remix-run/testing
合わせてvitest
、@testing-library/react
、@testing-library/jest-dom
、@testing-library/user-event
、jsdom
もインストールします。
npm install --save-dev @testing-library/react @testing-library/jest-dom @testing-library/user-event jsdom
npm install -D vitest
また、vitest.config.ts
も作成します。
import * as path from "path";
import * as VitestConfig from "vitest/config";
export default VitestConfig.defineConfig({
test: {
globals: true,
environment: "jsdom",
},
resolve: {
alias: {
"~": path.resolve(__dirname, "app"),
},
},
});
テストを作成
プロジェクト作成時
Remix のプロジェクトを作成した時点で、/app/routes/_index.tsx
のテストを作成してみます。
import { describe, expect, test } from "vitest";
import { createRemixStub } from "@remix-run/testing";
import { render, screen, waitFor } from "@testing-library/react";
import "@testing-library/jest-dom";
import Index from "~/routes/_index";
describe("_index.tsxのテスト", () => {
test("初期表示テスト", async () => {
const RemixStub = createRemixStub([{ path: "/", Component: Index }]);
render(<RemixStub />);
await waitFor(async () => {
expect(await screen.findByText("Welcome to Remix")).toBeVisible();
expect(
await screen.findByText("15m Quickstart Blog Tutorial")
).toBeVisible();
expect(
await screen.findByText("Deep Dive Jokes App Tutorial")
).toBeVisible();
expect(await screen.findByText("Remix Docs")).toBeVisible();
});
});
});
createRemixStub()
でスタブを作成し、render(<RemixStub />)
でレンダリングしています。レンダリングされた<RemixStub>
に対してテストを行なっています。
loader のテスト
/routes/test_loader.tsx
を作り、テストとして/tests/test_loader.test.tsx
を作成します。
import { json } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
export const loader = async () => {
return json({ message: "hogehoge" });
};
export default function TestLoader() {
const { message } = useLoaderData<typeof loader>();
return (
<>
<p>Text is {message}</p>
</>
);
}
import { describe, expect, test } from "vitest";
import { createRemixStub } from "@remix-run/testing";
import { render, screen, waitFor } from "@testing-library/react";
import "@testing-library/jest-dom";
import TestLoader, { loader } from "~/routes/test_loader";
describe("test_loader.tsxのテスト", () => {
test("『hogehoge』が表示される", async () => {
const RemixStub = createRemixStub([
{
path: "/",
Component: TestLoader,
loader,
},
]);
render(<RemixStub />);
await waitFor(async () => {
const element = await screen.findByText("Text is hogehoge");
expect(element).toBeVisible();
expect(element).toBeInTheDocument();
});
});
});
createRemixStub()
にtest_loader.tsx
のloader
を設定しています。
Remix 公式の Example には loader()を書いて json を返していますが、このように書くことtest_loader.tsx
に実際に書いたloader()
を呼び出すことができます。
test_loader.tsx
のloader()
では『hogehoge』を返しているだけなので、画面に『hogehoge』が表示されているかを確認しています。
action のテスト
/routes/test_action.tsx
を作り、テストとして/tests/test_action.test.tsx
を作成します。
import { ActionFunctionArgs, json } from "@remix-run/node";
import { Form, useActionData } from "@remix-run/react";
export default function TestAction() {
const actionData = useActionData<typeof action>();
return (
<Form method={"POST"}>
<div>
<label htmlFor="email">email</label>
<input type="email" name="email" id="email" placeholder="email" />
</div>
<div>
{actionData?.message != null && (
<p id="message">{actionData?.message}</p>
)}
</div>
<div>
<button type={"submit"}>Submit</button>
</div>
</Form>
);
}
export const action = async ({ request }: ActionFunctionArgs) => {
const formData = await request.formData();
const email = String(formData.get("email"));
if (!email.includes("hoge.co.jp"))
return json({ message: "メールアドレスが不正です!" });
return json({ message: "actionに成功しました!" });
};
import { describe, expect, test } from "vitest";
import { createRemixStub } from "@remix-run/testing";
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { userEvent } from "@testing-library/user-event";
import "@testing-library/jest-dom";
import TestAction, { action } from "~/routes/test_action";
describe("test_action.tsxのテスト", () => {
test("labelが『email』の入力フォームとボタン名が『Submit』のボタンが初期表示される", async () => {
const RemixStub = createRemixStub([
{
path: "/",
Component: TestAction,
action,
},
]);
render(<RemixStub />);
await waitFor(async () => {
const emailElement = screen.getByRole("textbox", { name: "email" });
const buttonElement = screen.getByRole("button", { name: "Submit" });
expect(emailElement).toBeVisible();
expect(emailElement).toBeInTheDocument();
expect(buttonElement).toBeVisible();
expect(buttonElement).toBeInTheDocument();
});
});
test("フォームの送信に失敗すると『メールアドレスが不正です!』が表示される", async () => {
const RemixStub = createRemixStub([
{
path: "/",
Component: TestAction,
action,
},
]);
render(<RemixStub />);
// 要素を取得。
const emailElement = screen.getByRole("textbox", { name: "email" });
// 適切ではないemailに変更。
fireEvent.change(emailElement, { target: { value: "hoge@fuga.co.jp" } });
// Submitボタンをクリック。
await userEvent.click(screen.getByRole("button", { name: "Submit" }));
await waitFor(async () => {
const messageElement = await screen.findByText(
"メールアドレスが不正です!"
);
expect(messageElement).toBeVisible();
expect(messageElement).toBeInTheDocument();
});
});
test("フォームの送信に成功すると『actionに成功しました!』が表示される", async () => {
const RemixStub = createRemixStub([
{
path: "/",
Component: TestAction,
action,
},
]);
render(<RemixStub />);
// 要素を取得。
const emailElement = screen.getByRole("textbox", { name: "email" });
// 適切なemailに変更。
fireEvent.change(emailElement, { target: { value: "hoge@hoge.co.jp" } });
// Submitボタンをクリック。
await userEvent.click(screen.getByRole("button", { name: "Submit" }));
await waitFor(async () => {
const messageElement = await screen.findByText("actionに成功しました!");
expect(messageElement).toBeVisible();
expect(messageElement).toBeInTheDocument();
});
});
});
上から順に見ていきます。
「label が『email』の入力フォームとボタン名が『Submit』のボタンが初期表示される」では、レンダリング後に email の入力フォームと Submit のボタンが表示されているかを確認しています。
「フォームの送信に失敗すると『メールアドレスが不正です!』が表示される」では、email フォームに『hoge@fuga.co.jp』を入力し、Submit ボタンをクリックします。action 内では『hoge.co.jp』が含まれていなければメールアドレスと判定しないので、画面上に『メールアドレスが不正です!』が表示されているかを確認しています。
「フォームの送信に成功すると『action に成功しました!』が表示される」では、email フォームに『hoge@hoge.co.jp』を入力し、Submit ボタンをクリックします。『hoge.co.jp』に一致するので『action に成功しました!』にが表示されているかを確認しています。
最後に
今回は Remix のテストを書いてみました。
loader や action のテストが書けるのはありがたいです!
テストの書き方や他に良いテスト方法がある場合はコメントや Issue を立ててもらえると嬉しいです!🙇♂️
Discussion