2025.05.19

Remix × Vitest で Login API をテストする

Shuji
- Vitest
- Remix
- React
はじめに
Vitest は、Jest のような API を備えた高速なテストフレームワークであり、Remix の loader や action のサーバーサイドのテストにも適用できます。
本記事では、Vitest を使用して login.tsx の loader と action をテストしてみました。
テスト対象の loader と action
login.tsx の loader と action は以下のような処理を行います。
loader- ユーザーがログイン済みなら
/にリダイレクト - ログインしていなければ
{}を返す (HTTP 200)
- ユーザーがログイン済みなら
actionemailやpasswordのバリデーションを行う- ユーザー認証 (
verifyLogin) を実行 - 認証成功ならセッションを作成し、指定のページ (
redirectTo) にリダイレクト
export const loader = async ({ request }: LoaderFunctionArgs) => {
const userId = await getUserId(request);
if (userId) return redirect("/");
return json({});
};
export const action = async ({ request }: ActionFunctionArgs) => {
const formData = await request.formData();
const email = formData.get("email");
const password = formData.get("password");
const redirectTo = safeRedirect(formData.get("redirectTo"), "/");
const remember = formData.get("remember");
if (!validateEmail(email)) {
return json(
{ errors: { email: "Email is invalid", password: null } },
{ status: 400 },
);
}
if (typeof password !== "string" || password.length === 0) {
return json(
{ errors: { email: null, password: "Password is required" } },
{ status: 400 },
);
}
if (password.length < 8) {
return json(
{ errors: { email: null, password: "Password is too short" } },
{ status: 400 },
);
}
const user = await verifyLogin(email, password);
if (!user) {
return json(
{ errors: { email: "Invalid email or password", password: null } },
{ status: 400 },
);
}
return createUserSession({
redirectTo,
remember: remember === "on" ? true : false,
request,
userId: user.id,
});
};必要なモック
セッション管理のモック
export const mockSession = {
getUserId: vi.fn<() => Promise<`email#${string}` | undefined>>(),
createUserSession: vi.fn<(
args: { redirectTo: string; remember: boolean; request: Request; userId: string }
) => Promise<Response>>(),
};ユーザー認証のモック
export const mockUser = {
verifyLogin:
vi.fn<
(
email: string,
password: string,
) => Promise<{ id: email#${string} } | null>
>(),
};loader のテスト
- ログイン済みの場合
/にリダイレクトすることを確認 - ログインしていない場合 HTTP 200 で
{}を返すことを確認
import { redirect } from "@remix-run/node";
import type { LoaderFunctionArgs } from "@remix-run/node";
import { describe, it, expect, vi } from "vitest";
import { mockSession } from "~mocks/auth/session.mock";
vi.mock("~/session.server", () => mockSession);
import { loader } from "~/routes/_auth+/login";
import { getUserId } from "~/session.server";
describe("loader", () => {
it("redirects to '/' if user is logged in", async () => {
vi.mocked(getUserId).mockResolvedValue("email#user-id");
const request = new Request("http://localhost/");
const args: LoaderFunctionArgs = { request, params: {}, context: {} };
const response = await loader(args);
expect(response).toEqual(redirect("/"));
});
it("returns an empty JSON response if user is not logged in", async () => {
vi.mocked(getUserId).mockResolvedValue(undefined);
const request = new Request("http://localhost/");
const args: LoaderFunctionArgs = { request, params: {}, context: {} };
const response = await loader(args);
expect(response.status).toBe(200);
const responseBody = await response.json();
expect(responseBody).toEqual({});
});
});action のテスト
- 無効なメールアドレス (
invalid-email) の場合400 Bad Requestを返し、適切なエラーメッセージを含むことを確認
- 正しいログイン情報 (
user@example.com) の場合verifyLoginが成功し、createUserSessionが適切な引数で呼び出され、リダイレクトされることを確認
import { redirect } from "@remix-run/node";
import type { ActionFunctionArgs } from "@remix-run/node";
import { describe, it, expect, vi } from "vitest";
import { mockSession } from "~mocks/auth/session.mock";
import { mockUser } from "~mocks/auth/user.mock";
vi.mock("~/models/user.server", () => mockUser);
vi.mock("~/session.server", () => mockSession);
import { verifyLogin } from "~/models/user.server";
import { action } from "~/routes/_auth+/login";
import { createUserSession } from "~/session.server";
describe("action", () => {
it("returns an error if email is invalid", async () => {
const formData = new FormData();
formData.append("email", "invalid-email");
formData.append("password", "password123");
const request = new Request("http://localhost/", {
method: "POST",
body: formData,
});
const args: ActionFunctionArgs = { request, params: {}, context: {} };
const response = await action(args);
const jsonResponse = await response.json();
expect(jsonResponse).toEqual({
errors: { email: "Email is invalid", password: null },
});
expect(response.status).toBe(400);
});
it("creates a user session on successful login", async () => {
vi.mocked(verifyLogin).mockResolvedValue({
id: "email#user-id",
email: "user@example.com",
});
vi.mocked(createUserSession).mockResolvedValue(redirect("/dashboard"));
const formData = new FormData();
formData.append("email", "user@example.com");
formData.append("password", "password123");
formData.append("redirectTo", "/dashboard");
const request = new Request("http://localhost/", {
method: "POST",
body: formData,
});
const args: ActionFunctionArgs = { request, params: {}, context: {} };
const response = await action(args);
expect(response).toEqual(redirect("/dashboard"));
expect(createUserSession).toHaveBeenCalledWith({
redirectTo: "/dashboard",
remember: false,
request,
userId: "email#user-id",
});
});
});そして、テストを実行してみます。
$ npx vitest run test/auth/login.action.test.ts
RUN v2.1.9
✓ test/auth/login.action.test.ts (2)
✓ action (2)
✓ returns an error if email is invalid
✓ creates a user session on successful login
Test Files 1 passed (1)
Tests 2 passed (2)
Start at 08:34:48
Duration 565ms (transform 90ms, setup 122ms, collect 104ms, tests 7ms, environment 136ms, prepare 39ms)まとめ
Vitest のモック機能 (vi.mock) を活用することで、データベースや API に依存せずに 効率的にテストできる ことができました。
今後は スナップショットテストやカバレッジ測定 などの機能や、MSW (Mock Service Worker) と組み合わせるのも良いですね!






![[Gatsby.js] gatsby-plugin-canonical-urlsでcanonicalタグを生成するのアイキャッチ画像](https://images.microcms-assets.io/assets/0e1a47976b6b480f93b35e041ab5fe03/7bfa208af2ba483abc6b8841e398f8b7/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88%202025-05-07%203.03.49.png)