BLOG ブログ


2026.02.05 TECH

Next.jsでVitest Browser Mode × MSW を使った単体テストの作成方法

Vitest 4:メジャーアップデート公開

2025年10月22日に【Vitest 4】のメジャーアップデートが公開されました。
目玉としては、Vitest Browser Mode が安定版になりました。

Vitest 4.0 is out!

Vitest Browser Mode は、ブラウザ上でテストを実行できるモードです。
以前までは、Node.js の環境でシミュレートされたブラウザ環境でテストを実行するため、実際のブラウザ環境と比べると差分が少しありました。
ですが、Vitest Browser Modeでは、実際のブラウザ環境でテストできるため、フロントエンドのテストの信頼性を向上でき、実際の挙動の確認も簡単になりました。

そこでこの記事では、Vitest Browser Mode の利用方法や実際の利用を想定し、MSWのモックを用いて、Client Components でのテストケースについて紹介します。

環境

  • Next.js: 16.0.7
  • React: 19.0.0
  • Node.js: 22.21.1
  • Vitest: 4.0.14
  • Tailwind CSS: 4

Vitest, Tailwind CSSはすでに導入済みの状態から始めます。
導入していない場合は、下記を参考にして設定してみてください。

How to set up Vitest with Next.js


Install Tailwind CSS with Next.js

Vitest Browser Modeの導入

まずは、Vitest Browser Mode を導入します。

Vitest Browser Mode 公式サイト

下記のコマンドを実行し、ブラウザモードの初期化を行います。

npx vitest init browser

コマンド実行後、いくつか質問が出ますが、自身の環境に合った選択肢を選びましょう。
もし @vitest/browser-playwright が自動で追加されなかった場合は手動で追加します。

npm install -D vitest @vitest/browser-playwright

そして、vitest.config.mts に、ブラウザテストを利用するために設定を追加します。

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:の中で設定できる項目は下記です。

  • enabled:true でブラウザモードを有効化
  • provider:playwright() で Playwright をテストランナーとして使用
  • instances:使用するブラウザを指定可能(Chromium,Firefox,Webkitなど)

今回は複雑な設定などは行わず、公式サイトの記載の設定にします。

次にvitest.setup.tsを作成し、app/globals.cssを読み込むようにします。
globals.cssにはTailwind CSS を読み込むために、@import "tailwindcss"; が無い場合は、記載をしましょう。

vitest.setup.ts

import "./app/globals.css";

これでブラウザ上で Reactコンポーネントの単体テストが実行できる準備ができました。

テストコードの確認と実行

Vitest Browser Mode を導入すると初期でテストコードがあるので、それを確認します。
内容としては、文字を表示するだけの HelloWorld.tsx とそのテストコードが作られています。
もしない場合は、下記を作成してください。

vitest-example/HelloWorld.tsx

import React from "react";

export default function HelloWorld({ name }: { name: string }) {
  return (
    <div>
      <h1>Hello {name}!</h1>
    </div>
  );
}

vitest-example/HelloWorld.test.tsx

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つのタブがあり、それぞれ色々な結果を見れます。

  • Report タブ
    • テストの結果や失敗した箇所を視覚的に確認可能
  • Module Graph タブ
    • ファイル間の依存関係をグラフで表示
  • Code タブ
    • 実際のテストコードを確認しながらデバッグ可能
  • Console タブ
    • consoleの結果を見れる

この4つのタブがあることで、テスト失敗時にミスの箇所が特定しやすくなります。
そのため、画面を見ながら安全にテストを作成することができます。

Client ComponentsでのMSWとVitestでのテスト

今回は、MSW という APIをモックするためのライブラリを導入して、実際にデータが返る状態でコンポーネントの単体テストを作成します。
コンポーネントは、React の Client Components で作成し、ブラウザ側でデータ取得を行います。
Client Components とは、【use client ディレクティブ】が付いていて、ブラウザ上で実行されるコンポーネントです。
それでは実際に作成しましょう。

※Client Components とは別のサーバー側で実行される Server Components もありますが、今回の記事としては取り上げません。

MSW 導入方法

まずはMSWの導入します。

npm install msw

mockServiceWorker.js を public に配置するためのコマンドを配置します。

npx msw init ./public --save

次にブラウザでAPIモックを有効にするためにsetupWorkerを追加します。
setupWorkerの引数に後ほど追加するhandlersを設定します。

mocks/browser.ts

import { setupWorker } from "msw/browser";
import { handlers } from "./handlers";

export const worker = setupWorker(...handlers);

そして、msw/browsersetupWorker が動くように設定します。

process.env.NEXT_PUBLIC_USE_MOCKtrue の場合に、worker.start(MSWの起動)するようにします。
これによりブラウザ上からのデータの取得は行えますが、このモックではサーバー上からはデータの取得できないので、注意しましょう。

mocks/MSWProvider.tsx

"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 を呼び出します。

app/layout.tsx

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 にブラウザ上で取得するときにデータを返すようにします。

mocks/handlers.ts

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-clientpage.tsx を作成します。

app/tests-client/page.tsx

import { PostList } from "./PostList";

export default function TestsPage() {
  return <PostList />;
}

次に PostListコンポーネントを作成し、単体テストを追加します。

PostList は、ブラウザ上で投稿データを取得して、そのデータを表示するシンプルなコンポーネントです。

app/tests-client/PostList.tsx

"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 が動くように設定します。

app/tests-client/PostList.test.tsx

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』ですが、ここでは意図的に遷移先『/』を期待するテストを記述します。

app/tests-client/PostList.test.tsx

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 を見ながら、右側でエラーも見れるため、通常のテストよりもデバックしやすく、原因を見つけやすいです。
今回の例だと遷移先のリンクが間違っていたので、テストコードを修正するとテストが成功します。

app/tests-client/PostList.test.tsx

{/* 省略 */}
await expect.element(getByText("リンク")).toHaveAttribute("href", "/test");
{/* 省略 */}

まとめ

Next.jsVitest Browser Mode を利用することで、単体テストを従来の Node.js環境だけでなく、ブラウザ上で実行できます。
これにより実際に近い形でのテストが可能になるため、テストの精度も高くなるため、ぜひ導入してみてください。


一覧に戻る


LATEST ARTICLE 最新の記事

CATEGORY カテゴリー