🐶

Nuxt3の継続できて辛くないユニットテスト方法

2024/08/29に公開

はじめに

私は普段、Nuxt3でフロントエンド開発をすることが多いのですが、単体テストの方法が確立しておりませんでした。

最近、自分なりの方法が確立したので、紹介します。

今回紹介する技術を使ってテストの準備をしていけば、Nuxt3で、効率的かつ継続が辛くないテストコードが書けますので、是非読んで実行してください!

できること

  • Nuxt3 × vitestでの単体テストの方法をすることができる。
  • テストファイルで、毎回お決まりの設定を書かないで済むようになる。
  • Nuxt環境での単体テストは、オートインポートやuseStateなどのモックが辛いが、それをほぼしないで良くなる。
  • APIのモックを楽に書くことができる。

テストコードの開発体験イメージ

以下のような流れで、テストコードを開発でいるようになります!
1.コマンド実行

2.質問されるので入力していく

3.テストファイルテンプレ生成!ここからテストを書いていく!

サンプルコード

結論から言ってしまいますが、以下のリポジトリが今回紹介する単体テストのサンプルコードになります。
この記事で色々と説明していきますが、コードさえ見れればいいと言う方はどうぞ。
https://github.com/mrs-y-arai/nuxt3-unit-test-sample

使用技術/ライブラリ

使用バージョンは、githubのsampleリポジトリをご確認ください。

vitest

https://vitest.dev/
vite環境で動く、JavaScriptのテストツールです。

viteで動いているので、動きが速いです。

他のテストツールで言うと、jestが挙げられますが、jestとほぼ使い方が同じなので、jestを使ったことがある人でも使いやすいと思います。

Nuxt test utils

https://github.com/nuxt/test-utils
Nuxt環境のテストを楽に実行することができる便利モジュールです。

vitestでテストを実行するとき、ブラウザは動かないので、Nuxtも起動しません。
Nuxtが起動しないと、Nuxtに備え付けの機能(useStateとか)も動きません。

Nuxtを使ったアプリケーションは、Nuxtに備え付けの機能がある前提で開発しているので、Nuxtを起動していないと、バグります。

その状態ではテストが正しくできないのですが、Nuxt test utilsを使えば、Nuxtが起動された状態でテストができます。

MSW(Mock Service Worker)

https://mswjs.io/
めちゃめちゃざっくり言うと、Service Workerを使って、APIをモックすることができるライブラリです。

「テスト用のダミーレスポンスを返してくれる」と言うイメージです。

例えば、GET: /api/v1/news リクエストをクライアントからapiへリクエストするとき、MSWで事前にダミーレスポンスを設定すると、そのダミーレスポンスを返してくれるような感じです。

apiをモックできるので、フロントエンドのテストをする時、めちゃ便利です。

※ Service Workerは、クライアントのページとサーバーの間にあるもので、MSWは、サーバーに到達する前に、apiの挙動をモックするような形で動いているような感じです。

Scaffdog

https://scaff.dog/
Scaffdogは、テンプレートをもとに、ファイルを生成してくれるライブラリです。

テストコードは、モックをしたり、お決まりの設定のようなコードがあり、毎度同じようなコードを結構書きます。

なので、テンプレートにしやすいと思い、Scaffdogでテンプレート化する方針を取りました。

ざっくりとした構成

構成で言うと、、
「vitestやMSWと言う、テストでよく使われるライブラリを使いつつ、Nuxt環境でテストを実行することのできるnuxt test utilsを使用する。Scaffdogでテストコードをテンプレ化し、効率的にテストを書く!」
と言う感じになります!

テストの準備

vitest・Nuxt test utilsで、テストの設定

基本設定

テストの基本設定をしていきます。

1.パッケージインストール

npm i -D vitest @vitest/coverage-v8 @nuxt/test-utils @testing-library/vue happy-dom dotenv

2.nuxt設定

nuxt.config.ts
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
  compatibilityDate: "2024-04-03",
  devtools: { enabled: true },
  srcDir: "src/",
  modules: ["@nuxt/test-utils/module"], // ここを追加する
  runtimeConfig: {
    public: {
      apiBase: "/api",
    },
  },
});

3.vitest.configの設定

vitest.config.ts
/// <reference types="vitest" />
import { defineVitestConfig } from '@nuxt/test-utils/config';
import dotenv from "dotenv";

export default defineVitestConfig({
  test: {
    environment: 'nuxt',
    // テスト対象ファイル
    include: ['src/**/*.spec.ts'],
    reporters: ['verbose'],
    coverage: { // カバレッジを設定すると、テストの網羅率が確認できるようになります。
      provider: 'v8',
      all: true,
      // カバレッジ対象ファイル
      include: [
        '**/composables/**/*.ts',
        '**/features/**/*.ts',
        '**/utils/**/*.ts',
      ],
      // カバレッジ対象外ファイル
      exclude: ['**/features/**/index.ts'],
      // テストレポートをどこに生成するか
      reportsDirectory: 'test-reports',
    },
    // テスト用のenvファイル読み込み
    env: dotenv.config({ path: ".env.test" }).parsed,
  },
});

4.テスト用のenv作成

NUXT_PUBLIC_APP_ENV=test

# APIのベースURL
NUXT_PUBLIC_API_BASE=http://localhost:3000/api

以上で、設定は完了です!

なぜ、Nuxt test utilsを使うか

正直、Nuxt test utilsがなくてもテストはできますが、Nuxt test utilsがないとテストを書くのがとても大変になります。

Nuxt test utilsはとても大事なモジュールなので、ここで少しNuxt test utilsの説明をします。

なぜ、Nuxt test utilsを使うかというと、「Nuxtランタイム環境でテストができるようになるから」です。

Nuxtランタイム環境でテストできるようになると、テストの準備がめちゃめちゃ楽になります。
逆にNuxtランタイム環境でテストできない場合、以下のことをしなければいけなく、それがとても大変です。

  • Nuxtの機能であるオートインポートを再現
  • Nuxtの備え付けのcomposables(useStateなど)をモックする

これらをやるには、以下の記事に書いてあることをやらなければいけません。
https://tech.andpad.co.jp/entry/2023/03/16/100000

1つ1つやっていくと、かなり時間がかかります。
テストコードを書きたいのに、テストの準備をするためにコードを書くという状態になりしんどくなります。
途中で骨が折れて、テストを書くことを諦めてしまうこともあります。。

そこで、Nuxt test utilsを使います。
Nuxt test utilsを使えば、オートインポートの再現や備え付けのcomposablesのモックをする必要がなくなります。

以上が、Nuxt test utilsが解決することです。

※ ただ、備え付けのcomposablesをモックする場合もあるので、やり方は紹介しておきます。

[やらなくて良いが一応紹介]備え付けcomposablesをモックする場合

useStateやuseRouteの返り値をモックしたいという場合は、以下のような感じでモックしてください。基本はやる必要ないです。

モックは、以下のような感じで設定をします。
1.パッケージインストール

npm i -D unplugin-auto-import

2.設定ファイルで、useRuntimeConfigのオートインポートのモックをする

vitest.config.ts
import path from 'path'; // 追加部分
import { defineVitestConfig } from '@nuxt/test-utils/config';
import AutoImport from 'unplugin-auto-import/vite'; // 追加部分

export default defineVitestConfig({
  test: {
    environment: 'nuxt',
    // テスト対象ファイル
    include: ['src/**/*.spec.ts'],
    reporters: ['verbose'],
    coverage: { // カバレッジを設定すると、テストの網羅率が確認できるようになります。
      provider: 'v8',
      all: true,
      // カバレッジ対象ファイル
      include: [
        '**/composables/**/*.ts',
        '**/features/**/*.ts',
        '**/utils/**/*.ts',
      ],
      // カバレッジ対象外ファイル
      exclude: ['**/features/**/index.ts'],
      // テストレポートをどこに生成するか
      reportsDirectory: 'test-reports',
    },
  },
  // ここから下追加部分
  plugins: [
    AutoImport({
      imports: [
        {
          '#app': ['useRuntimeConfig'],
        },
      ],
      dts: false,
    }),
  ],
  resolve: {
    alias: {
      '#app': path.resolve(__dirname, 'node_modules/nuxt/dist/app'),
    },
  },
});

3.テストコードでも、useRuntimeConfigのオートインポートのモックする

sample.test.ts
import {
  vi,
} from 'vitest';

vi.mock('#app', () => ({
  useRuntimeConfig: vi.fn().mockReturnValue({
    public: {
      apiBaseUrl: '/api/v1',
    },
  }),
}));

詳しく知りたい方は、先ほども紹介した、AND PADさんの記事を読み込むと良いです。
https://tech.andpad.co.jp/entry/2023/03/16/100000

MSWで、APIのモックを設定

次は、APIのモック設定です。
今回は、単体テストで使う場合の最小限の設定を記載します。

MSWの設定

1.パッケージインストール

npm i -D msw

2.package.json設定

package.json
{
  "name": "nuxt-app",
  "private": true,
  "type": "module",
  "scripts": {
    "build": "nuxt build",
    "dev": "nuxt dev",
    // 省略
  },
  "dependencies": {
	  // 省略
  }
  "devDependencies": {
	  // 省略
  }
  // ここから下を追加
  "msw": {
    "workerDirectory": "src/public"
  }
}

3.server.ts定義

/src/api/mocks/server.ts
import { setupServer } from "msw/node";
export const server = setupServer();

以上です。local環境で開発中に、APIをモックしたい場合は他にも色々設定が必要ですが、今回はテスト実行時にのみモックするように設定するので、msw側の設定はこれだけで大丈夫です。

テストケースごとにAPIのモックを作る

次に、テストケースごとにAPIのモックを作ります。

今回は、/api/v1/news にリクエストする場合のAPIモックを作ってみましょう。

一例として、テストファイルを丸っと書いてしまいます。こんな感じでモックをして、APIにリクエストする場合のテストを書いていきます。
コメントアウトで補足説明してます。

sample.spec.ts
import { http, HttpResponse } from "msw";
import {
  expect,
  it,
  describe,
  beforeAll,
  afterEach,
  afterAll,
} from "vitest";
import { server } from "~/api/mocks/server";

// 全てのテストを実行する前(1つ目のテスト開始前)に、モックサーバーを起動する。
beforeAll(() => {
  server.listen();
});

// 各テストが終わるたびに、モックサーバーのリクエストハンドラをリセットする。
afterEach(() => {
  server.resetHandlers();
});

// 全てのテストが終わったら、モックサーバーを閉じる。
afterAll(() => {
  server.close();
});

describe("fetchHelperのテスト", () => {
  it("お知らせが2件取得できること", async () => {
    // Arrange
    const {
      public: { apiBase },
    } = useRuntimeConfig();
    // APIののモック準備
    // APIのレスポンスを設定する。
    // jsonで以下のオブジェクトが返ってくる関数
    const getNewsListMock = () => {
      return HttpResponse.json({
        data: [
          {
            id: 1,
            title: "テストタイトル",
            body: "テストボディ",
          },
          {
            id: 2,
            title: "テストタイトル2",
            body: "テストボディ2",
          },
        ],
      });
    };
    // http://localhost:3000/api/v1/newsにGETリクエストが飛んできたら、
    // getNewsListMockを実行する。
    server.use(http.get(`${apiBase}/news`, getNewsListMock));

    // Act
    // fetchHelper関数で、http://localhost:3000/api/v1/newsにGETリクエストをする。
    const result = await fetchHelper(`/news`, {
      method: "GET",
    });

    // Assert
    expect(result.data).toHaveLength(2);
  });
});

Scaffdogの設定・テストコードのテンプレートを作成

次で最後です。

Scaffdogと言うライブラリを使うと、ファイルをテンプレート化して、生成して行くことが容易にできるようになります。

ここの設定まで終えれば、記事の冒頭で紹介した、コマンド実行をして、テストテンプレートファイルを生成することができます。
あとは各々テストを書いていくだけになります!

Scaffdogの導入/設定

1,インストール

npm i -D scaffdog

2.scaffdogを初期化
Please enter a document nameと聞かれるので、適当な名前をつけてください。
ひとまず、「MyTest」としてください。

npx scaffdog init

テンプレートファイルの作成

以下のような形で、テンプレートファイルを書きます。
補足説明は、コメントに残してます。

1.MyTest.md作成

.scaffdog/MyTest.md
---
name: "MyTestTemplate"
root: "./src"
output: "/"
ignore: []
questions: // ← テンプレートファイル作成時に聞かれる質問
  path: "Please enter test file path."
        // ↑「Please enter test file path.」の質問で入力されたものを、inputs.pathで参照できる
  name: "Please enter test file name."
        // ↑「Please enter test file name.」の質問で入力されたものを、inputs.nameで参照できる
---

// 質問で入力されたpath・nameで、テストファイルを生成
// 今回の場合、 ./src/{{inputs.path}} に、 テストファイルが生成される
# `{{inputs.path}}/{{inputs.name}}.spec.ts`

``` ← コピペ時に、半角にしてください。
{{ 'templates/UnitTestTemplate.ts' | read }}
// ↑ UnitTestTemplate.tsの中身が、生成されるテストファイルに書き込まれる。
``` ← コピペ時に、半角にしてください。

2.UnitTestTemplate.tsを作成
生成するテンプレートファイルの中身を設定します。
ベース実装は以下になります。

APIのモックや、useRuntimeConfigのモック、その他のテストなど使いまわせるものをテンプレートにしているのがポイントです。

UnitTestTemplate.ts
// 複製後、以下の2行は削除
/* eslint-disable */
// @ts-nocheck
import { http, HttpResponse } from "msw";
import {
  expect,
  it,
  describe,
  beforeAll,
  afterEach,
  afterAll,
} from "vitest";
import { server } from "~/api/mocks/server";

// 全てのテストを実行する前(1つ目のテスト開始前)に、モックサーバーを起動する。
beforeAll(() => {
  server.listen();
});

// 各テストが終わるたびに、モックサーバーのリクエストハンドラをリセットする。
afterEach(() => {
  server.resetHandlers();
});

// 全てのテストが終わったら、モックサーバーを閉じる。
afterAll(() => {
  server.close();
});

describe("テストする対象の関数", () => {
  it("テストケース", () => {
    // Arrange
    // Act
    // Assert
    expect(result).toBe("");
  });
});

describe("apiをモックする場合", () => {
  it("テストケース", () => {
    // Arrange
    // APIののモック準備
    const {
      public: { apiBaseUrl },
    } = useRuntimeConfig();
    // apiのモック関数
    const getNewsListMock = () => {};
    server.use(http.get(`${apiBaseUrl}/news`, getNewsListMock));

    // Act

    // Assert
    // const expectedValue = {};
    // expect(data).toHaveLength(50);
  });
});

package.jsonに、scaffdogのコマンドを登録

package.json
{
  "name": "nuxt3-starter-kit",
  "version": "1.0.0",
  "private": true,
  "type": "module",
  "engines": {
    "node": ">=20.x"
  },
  "scripts": {
    "dev": "npm run prepare && nuxt dev --dotenv .env.local",
    "build": "nuxt generate",
    "preview": "nuxt preview",
    "test": "vitest",
    "test:coverage": "vitest --coverage",
    "typecheck": "nuxt typecheck",
    "scaffdog": "scaffdog generate" ← これを追加
  },
}

ここまでいけば、npm run scaffdogをすれば、テストコードのテンプレートを生成できます!

テストコードを書いていく

ここまでで準備したら、後はテストコードを書いていきます。
以下の手順でコードを書いていきます!

  1. Scaffdogで、テストファイルのテンプレートを生成する( npm run scaffdog )
  2. 生成したテンプレートを参考にして、APIのモック等を設定する
  3. 検証したいテストを書く
  4. テスト実施( npm run vitest:coverage )

まとめ

継続できてかつ効率的なテスト方法を考えた結果、この方法に落ち着きました。

Nuxt3環境のテストは色々と面倒なことがありましたが、便利なライブラリのおかげでだいぶ楽にできることがわかりました。

これからもアップデートしていきたいので、他に良い方法があれば知りたいです。

次は結合テストの良い方法を考えていきたいと思います。

Vue・Nuxt 情報が集まる広場 / Plaza for Vue・Nuxt.

Discussion