くらげになりたい。

くらげのようにふわふわ生きたい日曜プログラマなブログ。趣味の備忘録です。

VitestでNitroをテストする(+Firebase Emulator)

最近、Nitroでサーバを実装しているけど、
テスト周りでハマったので、いろいろ調べてみたときの備忘録(*´ω`*)

現状、テストに関するガイドはないっぽい

このディスカッションくらいしかないっぽい。。

Nitro自体のテストコードは用意されているが、
ビルドなども含まれているので、これをそのまま使うのは少し冗長。

test/tests.tstest/presets/node.test.tsを読んでいくと、

以下のような形で、ビルドされたサーバコード(.output/server/index.msj)を起動しているよう。

const entryPath = resolve(ctx.outDir, "server/index.mjs");
const { listener } = await import(entryPath);

await startServer(ctx, listener);

なので、これを活用してテストコードを書くようにしてみる。

テストコード

Vitestのテストコードはこんな感じ。

// __tests__/apis.test.ts
import { describe, expect, it } from "vitest";
import { fetch as ofetch } from "ofetch";
import { joinURL } from "ufo";

// テスト用のClient Util: baseUrlを省略のため
const createClient = (baseUrl: string = "http://localhost:3000") => ({
  fetch: (url: string, opts?: RequestInit) =>
    ofetch(joinURL(baseUrl, url.slice(1)), opts),
});

describe("APITest", async () => {
  const ctx = createClient();

  it("404", async () => {
    const res = await ctx.fetch("/not_found");
    const data = await res.json();
    expect(res.status).toBe(404);
    expect(data).toEqual({
      error: { message: "Error: Cannot find any path matching /not_found." },
    });
  });
});

そのままだとサーバが起動していないので、エラーになる。

global-setup: サーバの起動

なので、Nitroのテストコードを参考に、
サーバを起動する処理を追加していく。

./.output/server/index.mjsを参照するので、
ビルド後(nitropack build)じゃないといけない。

// __tests__/common/global-setup.ts
import { resolve } from "pathe";

export async function setup() {
  const entryPath = resolve("./.output/server/index.mjs");
  await import(entryPath);
}

サーバの起動は、テスト時に全体で1度だけにしたい。

VitestのglobalSetupを使って起動するようにしておく。

// vitest.config.ts
import { defineConfig } from "vitest/config";

export default defineConfig({
  test: {
    include: ["**/__tests__/**/*.test.ts"],
    globalSetup: ["./__tests__/common/global-setup.ts"],
  },
});

beforeAllなどを使うと、複数回起動されてしまうので、
ポートが重複して、起動時にエラーになる。。

Nitroのテストコードでは、
ビルドから行っていて、各テストコードごとに.output/が異なるようにしているっぽい?

package.jsonのscripts

準備ができたので、scriptsに追加する。

{
  "type": "module",
  "scripts": {
    // ...略
    // for testing
    "test": "pnpm vitest-es",
    "vitest-es": "NODE_OPTIONS=\"--enable-source-maps --experimental-vm-modules\" vitest --single-thread --run"
  },
  "dependencies": {
    "nitropack": "^2.5.2"
  },
  "devDependencies": {
    "typescript": "^5.1.6",
    "vitest": "^0.34.1"
  }
}

あとは、pnpm build && pnpm testなどすればOK。

Turborepoとかを使うと、
ビルドをキャッシュしてくれたりするので便利。

Firebase Emulatorに接続してテストする

FirebaseのAdmin SDKを使うことも多いので、
Firebase Emulatorに接続してテストしたい。

エミュレータに接続してもらうために、環境変数を設定しておく。

# .env.test
GCLOUD_PROJECT="my-test-project"
FIREBASE_AUTH_EMULATOR_HOST="127.0.0.1:9099"
FIRESTORE_EMULATOR_HOST="127.0.0.1:8080"
FIREBASE_STORAGE_EMULATOR_HOST="127.0.0.1:9199"
FIREBASE_DATABASE_EMULATOR_HOST="127.0.0.1:9000"

あとは、firebase emulators:execenv-cmdを使って、
エミュレータを起動しつつ、テストを実行するようにすればOK

{
  "type": "module",
  "scripts": {
    // ...略
    // for testing
    "test": "env-cmd -f .env.test pnpm em:run 'pnpm vitest-es'",
    "em:run": "firebase emulators:exec --only auth,firestore --project=my-test-project",
    "vitest-es": "NODE_OPTIONS=\"--enable-source-maps --experimental-vm-modules\" vitest --single-thread --run"
  },
  "dependencies": {
    "nitropack": "^2.5.2"
  },
  "devDependencies": {
    "typescript": "^5.1.6",
    "vitest": "^0.34.1",
    "env-cmd": "^10.1.0",
    "firebase-admin": "^11.10.1"
  }
}

以上!! これで一通りは準備が整った気がする。。(*´ω`*)

参考にしたサイトさま