VitestとSupertestによるExpress APIテストの実践


みなさん、こんにちは!クラウドソリューション開発部の寺谷です。
7月に入り、北海道もいよいよ夏本番という気温になってきましたね。暑い日に飲む冷たい炭酸水は最高ですが、お腹を壊さないように体調管理には気をつけていきます!

Vitestというと、ViteやReactと一緒に使うフロントエンド向けの印象が強いかもしれません。ですが、今回触ってみて「バックエンドのAPIテストでもかなり使いやすい」と感じました。

この記事では、カレンダーアプリのバックエンドAPIを例に、VitestとSupertestを使ってExpress APIのテストを書いた話を紹介します。

今回のサンプルアプリの構成は以下です。

  • バックエンド: Express + TypeScript
  • フロントエンド: React + TypeScript
  • データベース: PostgreSQL
  • 実行環境: Podman Compose
  • テスト: Vitest + Supertest

1. Vitestをバックエンドで使ってみた理由

もともとVitestは、Viteベースの高速なテストフレームワークです。
describeitexpect など、Jestに近い書き方ができるため、Jestを使ったことがある人ならかなり入りやすいと思います。

今回バックエンドで使ってみようと思った理由は、主に以下です。

  • TypeScriptでそのままテストを書きたい
  • Express APIをSupertestで確認したい
  • テストの実行を軽くしたい
  • フロントエンド以外でもVitestが使えるか試したい

実際に使ってみると、設定もシンプルで、Express APIのテストにも自然に使えました。

2. Vitestの導入方法と設定

2-1. Vitestのインストール

「バックエンドにVitestを入れるのって面倒そう…」と思われるかもしれませんが、手順は驚くほどシンプルです。

  • vitest: テストを実行し、結果を検証・集計するためのフレームワーク
  • supertest & @types/supertest: Expressアプリを擬似的に叩くためのライブラリ

2-2. 設定ファイル (vitest.config.ts) の作成

バックエンド配下に vitest.config.ts を作成し、以下のように記述します。

ポイントは environment: "node" の指定です。Express APIのテストなので、ブラウザ環境をシミュレートする jsdom ではなく、Node.js環境で実行するようにしています。

2-3. package.json へのスクリプト追加

テストを簡単に実行できるよう、scripts にコマンドを登録しておきます。

  • type-check: TypeScriptの型チェックを実行します。
  • test: テストを一発実行して終了します(CI環境などに便利)。
  • test:watch: ファイルの変更を監視し、修正するたびに自動でテストを再実行します。

3. テスト対象のAPI

今回作成したのは、スケジュールを管理するシンプルなカレンダーAPIです。

  • GET /api/health: APIが正常に起動しているか確認する
  • GET /api/events: 登録されている予定一覧を取得する
  • GET /api/events/:id: 指定したIDの予定詳細を取得する
  • POST /api/events: 新しい予定を登録する
  • PUT /api/events/:id: 指定したIDの予定を更新する
  • DELETE /api/events/:id: 指定したIDの予定を削除する

テストでは、正常系だけでなく異常系も確認しました。
例えば、以下のような内容です。

  • 予定一覧が日付、時刻、ID順で返ること
  • 存在しないIDでは404になること
  • 有効な予定情報なら作成できること
  • 必須項目や形式が不正なら400になること
  • 更新や削除ができること
  • 削除後は同じIDで取得できないこと

APIは「正常に動くこと」だけでなく、「エラーが起きたときにどう返すか」も同じくらい重要だと思います。
フロントエンドや外部システムと連携するバックエンドにおいて、エラー時のステータスコードやレスポンス形式が綺麗に共通化されていることは、呼び出し側の実装コストを減らすことにもつながるかと思います。

4. DBに依存しないテストにした

今回のテストでは、実際のPostgreSQLには接続していません。
代わりに、インメモリのリポジトリを使ってテスト用のExpressアプリを作成しています。

この形にしておくと、テスト側では次のように簡単にアプリを準備できます。

初期データが必要な場合も、配列を渡すだけです。

DBまで含めたテストも大事ですが、すべてのAPIテストでDBを使うと準備が重くなります。
今回は「APIとして期待したレスポンスを返すか」を素早く確認したかったため、インメモリで十分だと判断しました。
このおかげで、テストの実行も軽く、書いていてストレスが少なかったです。

5. テストデータはfactoryに分けた

APIテストを書いていると、リクエストデータや初期データがどんどん増えていきます。
最初はテストケースの中に直接書いてもよいのですが、増えてくると「結局このテストは何を確認したいんだっけ?」となりがちです。
そこで今回は、テストデータ生成をfactoryに分けました。

基本データを用意しておき、テストごとに変えたいところだけ上書きします。
例えば、バリデーションエラーを確認したい場合はこう書けます。

この形にしておくと、不正値にしているポイントがすぐに分かります。
テストデータを分けるだけで、テスト本体がかなり読みやすくなりました。

6. SupertestでAPIを叩く

Express APIのテストにはSupertestを使いました。
Supertestを使うと、実際にサーバーをポートで起動しなくても、Expressアプリに対してHTTPリクエストのようにテストできます。
例えば、予定作成APIのテストは以下のようになります。

request(app).post("/api/events").send(payload) のように書けるので、APIを呼び出している感覚に近いです。
ControllerやServiceの流れも含めて確認できるため、単純な関数テストよりも実際の利用に近い安心感がありました。

7. Gherkin風にして読みやすくした

今回のテストでは、テスト名をGherkin風にしています。
具体的には、以下のような形です。

  • Feature: 何の機能か
  • Context: どんな条件か
  • Scenario: 何を確認するか
  • Given: 前提
  • When: 操作
  • Then: 期待結果

テストコードは、動けばよいだけではなく、後から読んだときに仕様が分かることも大切だと思っています。
特にAPIテストは、画面よりも仕様が見えづらいことがあります。だからこそ、テスト名やコメントで「どういう条件で、何をしたら、何が返るのか」を残しておくと、かなり読みやすくなります。

実際、今回のようにGherkin風に書くと、テスト結果の表示も仕様一覧のように見えます。

8. 使ってみて感じたこと

Vitestをバックエンドテストで使ってみて、思っていたより自然に使えるなと感じました。

  • 設定がシンプル
  • Supertestと組み合わせやすい
  • TypeScriptでも書きやすい
  • Gherkin風にすると仕様として読みやすい

一方で、テストデータをそのまま書きすぎると、すぐに読みづらくなります。
そのため、factoryを用意したり、テスト用アプリを作るhelperを分けたりして、テストケース本体をできるだけ読みやすくすることが大事だと思いました。
また、正常系だけでなく、404や400のような異常系も早めに書いておくと安心です。

9. まとめ

今回は、Vitestを使ってExpress APIのバックエンドテストを書いてみた話を紹介しました。
Vitestはフロントエンド向けの印象がありましたが、environment: "node" を指定すればバックエンドでも問題なく使えます。
Supertestと組み合わせることで、Express APIの振る舞いをシンプルに確認できました。
今回やってよかったと感じたことは、以下です。

  • DBに依存しないテスト用アプリを用意したこと
  • テストデータをfactoryに分けたこと
  • Gherkin風にして仕様としても読みやすくしたこと

テストは、ただ数を増やすものではなく、安心して変更するための土台だと考えています。
今回のように、APIの重要な振る舞いを軽く確認できる形にしておくと、実装を変えるときの不安を減らすことができると思いました。

エコモットでは、モノづくりに共感してくれる仲間を募集中です!弊社に少しでも興味がある方、ぜひ下記の採用ページをご覧ください!