2025年10月22日に【Vitest 4】のメジャーアップデートが公開されました。
目玉としては、Vitest Browser Mode が安定版になりました。
Vitest Browser Mode は、ブラウザ上でテストを実行できるモードです。
以前までは、Node.js の環境でシミュレートされたブラウザ環境でテストを実行するため、実際のブラウザ環境と比べると差分が少しありました。
ですが、Vitest Browser Modeでは、実際のブラウザ環境でテストできるため、フロントエンドのテストの信頼性を向上でき、実際の挙動の確認も簡単になりました。
そこでこの記事では、Vitest Browser Mode の利用方法や実際の利用を想定し、MSWのモックを用いて、Client Components でのテストケースについて紹介します。
Vitest, Tailwind CSSはすでに導入済みの状態から始めます。 導入していない場合は、下記を参考にして設定してみてください。
How to set up Vitest with Next.js
Install Tailwind CSS with Next.js
まずは、Vitest Browser Mode を導入します。
下記のコマンドを実行し、ブラウザモードの初期化を行います。
npx vitest init browserコマンド実行後、いくつか質問が出ますが、自身の環境に合った選択肢を選びましょう。
もし @vitest/browser-playwright が自動で追加されなかった場合は手動で追加します。
npm install -D vitest @vitest/browser-playwrightそして、vitest.config.mts に、ブラウザテストを利用するために設定を追加します。
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
import { playwright } from '@vitest/browser-playwright'
export default defineConfig({
plugins: [react()],
test: {
browser: {
enabled: true,
provider: playwright(),
instances: [
{ browser: 'chromium' },
],
},
setupFiles: ['./vitest.setup.ts'],
},
})browser:の中で設定できる項目は下記です。
今回は複雑な設定などは行わず、公式サイトの記載の設定にします。
次にvitest.setup.tsを作成し、app/globals.cssを読み込むようにします。
globals.cssにはTailwind CSS を読み込むために、@import "tailwindcss"; が無い場合は、記載をしましょう。
import "./app/globals.css";これでブラウザ上で Reactコンポーネントの単体テストが実行できる準備ができました。
Vitest Browser Mode を導入すると初期でテストコードがあるので、それを確認します。
内容としては、文字を表示するだけの HelloWorld.tsx とそのテストコードが作られています。
もしない場合は、下記を作成してください。
import React from "react";
export default function HelloWorld({ name }: { name: string }) {
return (
<div>
<h1>Hello {name}!</h1>
</div>
);
}import { expect, test } from 'vitest'
import { render } from 'vitest-browser-react'
import HelloWorld from './HelloWorld.tsx'
test('renders name', async () => {
const { getByText } = await render(<HelloWorld name="Vitest" />)
await expect.element(getByText('Hello Vitest!')).toBeInTheDocument()
})コンポーネントに Hello Vitest! が存在しているかのシンプルなテストが記載されています。
普通の vitest のテストコードと異なる点としては、render は vitest-browser-react からインポートしており、browserテストを想定していることが分かります。
そして、下記のコマンドでテストを実行します。
npx vitest --browser=chromiumすると、自動的に Chromium が起動し、テスト結果をブラウザ上で確認できます。

テストは成功したことが分かります。
もし『Error: Cannot find package 'playwright' imported』と表示されたら、下記を実行しましょう。
npm install playwright画面を見ると3つの要素に分かれており、
左側:テストの実行結果の一覧が表示されており、どのテストが成功、失敗したかすぐに分かります。
中央 Browser UI:それぞれのテストケースごとのテストを実行した後の画面が表示されます。
成功した場合には、どのようなUIで表示されるか見れるため、便利です。
右側:4つのタブがあり、それぞれ色々な結果を見れます。
この4つのタブがあることで、テスト失敗時にミスの箇所が特定しやすくなります。
そのため、画面を見ながら安全にテストを作成することができます。
今回は、MSW という APIをモックするためのライブラリを導入して、実際にデータが返る状態でコンポーネントの単体テストを作成します。
コンポーネントは、React の Client Components で作成し、ブラウザ側でデータ取得を行います。
Client Components とは、【use client ディレクティブ】が付いていて、ブラウザ上で実行されるコンポーネントです。
それでは実際に作成しましょう。
※Client Components とは別のサーバー側で実行される Server Components もありますが、今回の記事としては取り上げません。
まずはMSWの導入します。
npm install mswmockServiceWorker.js を public に配置するためのコマンドを配置します。
npx msw init ./public --save次にブラウザでAPIモックを有効にするためにsetupWorkerを追加します。 setupWorkerの引数に後ほど追加するhandlersを設定します。
import { setupWorker } from "msw/browser";
import { handlers } from "./handlers";
export const worker = setupWorker(...handlers);そして、msw/browser の setupWorker が動くように設定します。
process.env.NEXT_PUBLIC_USE_MOCK が true の場合に、worker.start(MSWの起動)するようにします。
これによりブラウザ上からのデータの取得は行えますが、このモックではサーバー上からはデータの取得できないので、注意しましょう。
"use client";
import { useEffect, useState } from "react";
export default function MSWProvider({
children,
}: {
children: React.ReactNode;
}) {
const [isReady, setIsReady] = useState(false);
useEffect(() => {
if (process.env.NODE_ENV !== "development") {
return;
}
import("./browser").then(({ worker }) => {
worker
.start()
.finally(() => setIsReady(true));
});
}, []);
if (!isReady) return null;
return <>{children}</>;
}そして、layout.tsx で作成した MSWProvider を呼び出します。
import type { Metadata } from "next";
import MSWProvider from "../mocks/MSWProvider";
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="ja">
<body>
<MSWProvider>{children}</MSWProvider>
</body>
</html>
);
}最後に handlers を追加します。
今回は /posts にブラウザ上で取得するときにデータを返すようにします。
import { http, HttpResponse } from "msw";
export const handlers = [
http.get("*/posts", () => {
return HttpResponse.json([
{
id: 1,
title: "Mocked Post 1",
body: "This is a mocked post body.",
userId: 1,
},
{
id: 2,
title: "Mocked Post 2",
body: "This is another mocked post body.",
userId: 2,
},
]);
}),
];これで MSWの準備は完了しました。
画面で確認するために tests-client で page.tsx を作成します。
import { PostList } from "./PostList";
export default function TestsPage() {
return <PostList />;
}次に PostListコンポーネントを作成し、単体テストを追加します。
PostList は、ブラウザ上で投稿データを取得して、そのデータを表示するシンプルなコンポーネントです。
"use client";
import React, { useEffect, useState } from "react";
type Post = {
id: number;
title: string;
body: string;
userId: number;
};
export const PostList = () => {
const [posts, setPosts] = useState<Post[]>([]);
useEffect(() => {
fetch("/posts")
.then((res) => res.json())
.then((data) => setPosts(data))
}, []);
return (
<div>
<div className="flex flex-wrap justify-center m-4 gap-4">
{posts.map((post) => (
<div key={post.id} className="border p-4 rounded w-72 flex flex-col">
<h2 className="text-xl font-semibold">{post.title}</h2>
<p className="text-gray-600 flex-grow">{post.body}</p>
<p className="text-xs text-gray-400 mt-2">User ID: {post.userId}</p>
</div>
))}
</div>
<a href="/test" className="block text-center">リンク</a>
</div>
);
};テストケースを作成します。
テストケースでも MSW が動くように設定します。
import React from "react";
import { afterAll, beforeAll, expect, test } from "vitest";
import { render } from "vitest-browser-react";
import { worker } from "../../mocks/browser";
import { PostList } from "./PostList";
// モック開始
beforeAll(async () => {
await worker.start();
});
// モック停止
afterAll(() => {
worker.stop();
});
test("テキストが表示されるか", async () => {
const { getByText } = await render(<PostList />);
await expect.element(getByText("Mocked Post 1")).toBeVisible();
await expect.element(getByText("This is a mocked post body.")).toBeVisible();
});下記のコマンドを実行して、モックが動く状態でローカル環境を立ち上げます。
npm run dev/tests-client にアクセスして、データが取得されているか確認します。

問題がなければ、次にテストコマンドを実行します。
npx vitest --browser=chromiumすると PostList のテストケースは成功したことが分かります。
MSW などのモックがある場合でも、ブラウザ上で確認できるのはかなり便利です。

試しに失敗するテストケースを1つ追加します。
実際の実装では aタグの遷移先は『/test』ですが、ここでは意図的に遷移先『/』を期待するテストを記述します。
import React from "react";
import { afterAll, beforeAll, expect, test } from "vitest";
import { render } from "vitest-browser-react";
import { worker } from "../../mocks/browser";
import { PostList } from "./PostList";
// モック開始
beforeAll(async () => {
await worker.start();
});
// モック停止
afterAll(() => {
worker.stop();
});
test("テキストが表示されるか", async () => {
const { getByText } = await render(<PostList />);
await expect.element(getByText("Mocked Post 1")).toBeVisible();
await expect.element(getByText("This is a mocked post body.")).toBeVisible();
});
test("リンクがaタグのリンク先が正しいか", async () => {
const { getByText } = await render(<PostList />);
await expect.element(getByText("リンク")).toHaveAttribute("href", "/");
});それで再度テストコードを実行して、確認すると、ターミナル上でもエラーが出ていますが、ブラウザ上でもエラーを確認できます。
npx vitest --browser=chromium
Browser UI で、実際の UI を見ながら、右側でエラーも見れるため、通常のテストよりもデバックしやすく、原因を見つけやすいです。
今回の例だと遷移先のリンクが間違っていたので、テストコードを修正するとテストが成功します。
{/* 省略 */}
await expect.element(getByText("リンク")).toHaveAttribute("href", "/test");
{/* 省略 */}Next.js と Vitest Browser Mode を利用することで、単体テストを従来の Node.js環境だけでなく、ブラウザ上で実行できます。
これにより実際に近い形でのテストが可能になるため、テストの精度も高くなるため、ぜひ導入してみてください。