みなさん、こんにちは!クラウドソリューション開発部の寺谷です。
7月に入り、北海道もいよいよ夏本番という気温になってきましたね。暑い日に飲む冷たい炭酸水は最高ですが、お腹を壊さないように体調管理には気をつけていきます!
Vitestというと、ViteやReactと一緒に使うフロントエンド向けの印象が強いかもしれません。ですが、今回触ってみて「バックエンドのAPIテストでもかなり使いやすい」と感じました。
この記事では、カレンダーアプリのバックエンドAPIを例に、VitestとSupertestを使ってExpress APIのテストを書いた話を紹介します。
今回のサンプルアプリの構成は以下です。
- バックエンド: Express + TypeScript
- フロントエンド: React + TypeScript
- データベース: PostgreSQL
- 実行環境: Podman Compose
- テスト: Vitest + Supertest
1. Vitestをバックエンドで使ってみた理由
もともとVitestは、Viteベースの高速なテストフレームワークです。
describe、it、expect など、Jestに近い書き方ができるため、Jestを使ったことがある人ならかなり入りやすいと思います。
今回バックエンドで使ってみようと思った理由は、主に以下です。
- TypeScriptでそのままテストを書きたい
- Express APIをSupertestで確認したい
- テストの実行を軽くしたい
- フロントエンド以外でもVitestが使えるか試したい
実際に使ってみると、設定もシンプルで、Express APIのテストにも自然に使えました。
2. Vitestの導入方法と設定
2-1. Vitestのインストール
「バックエンドにVitestを入れるのって面倒そう…」と思われるかもしれませんが、手順は驚くほどシンプルです。
|
1 2 |
npm install -D vitest supertest @types/supertest |
- vitest: テストを実行し、結果を検証・集計するためのフレームワーク
- supertest & @types/supertest: Expressアプリを擬似的に叩くためのライブラリ
2-2. 設定ファイル (vitest.config.ts) の作成
バックエンド配下に vitest.config.ts を作成し、以下のように記述します。
|
1 2 3 4 5 6 7 8 9 10 11 |
import { defineConfig } from "vitest/config"; export default defineConfig({ test: { environment: "node", coverage: { reporter: ["text", "html"], }, }, }); |
ポイントは environment: "node" の指定です。Express APIのテストなので、ブラウザ環境をシミュレートする jsdom ではなく、Node.js環境で実行するようにしています。
2-3. package.json へのスクリプト追加
テストを簡単に実行できるよう、scripts にコマンドを登録しておきます。
|
1 2 3 4 5 6 7 8 |
{ "scripts": { "type-check": "tsc --noEmit", "test": "vitest run", "test:watch": "vitest" } } |
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アプリを作成しています。
|
1 2 3 4 5 6 7 8 9 10 |
import { createApp } from "../../src/app.js"; import type { CalendarEvent } from "../../src/models/calendarEvent.js"; import { createInMemoryEventRepository } from "../../src/repositories/eventRepository.js"; export function createTestApp(initialEvents: CalendarEvent[] = []) { return createApp({ eventRepository: createInMemoryEventRepository(initialEvents), }); } |
この形にしておくと、テスト側では次のように簡単にアプリを準備できます。
|
1 2 |
const app = createTestApp(); |
初期データが必要な場合も、配列を渡すだけです。
|
1 2 |
const app = createTestApp(createCalendarEvents()); |
DBまで含めたテストも大事ですが、すべてのAPIテストでDBを使うと準備が重くなります。
今回は「APIとして期待したレスポンスを返すか」を素早く確認したかったため、インメモリで十分だと判断しました。
このおかげで、テストの実行も軽く、書いていてストレスが少なかったです。
5. テストデータはfactoryに分けた
APIテストを書いていると、リクエストデータや初期データがどんどん増えていきます。
最初はテストケースの中に直接書いてもよいのですが、増えてくると「結局このテストは何を確認したいんだっけ?」となりがちです。
そこで今回は、テストデータ生成をfactoryに分けました。
|
1 2 3 4 5 6 7 8 9 10 11 12 |
export function createCalendarEventInput( overrides: Partial<CalendarEventInput> = {}, ): CalendarEventInput { return { title: "Vitestを書く", date: "2026-07-10", time: "10:30", description: "APIの作成テストを追加する。", ...overrides, }; } |
基本データを用意しておき、テストごとに変えたいところだけ上書きします。
例えば、バリデーションエラーを確認したい場合はこう書けます。
|
1 2 3 4 5 6 |
createCalendarEventInput({ title: " ", date: "2026-02-31", time: "25:00", }); |
この形にしておくと、不正値にしているポイントがすぐに分かります。
テストデータを分けるだけで、テスト本体がかなり読みやすくなりました。
6. SupertestでAPIを叩く
Express APIのテストにはSupertestを使いました。
Supertestを使うと、実際にサーバーをポートで起動しなくても、Expressアプリに対してHTTPリクエストのようにテストできます。
例えば、予定作成APIのテストは以下のようになります。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
describe("Feature: 予定API", () => { describe("Context: 有効な予定情報で予定作成APIへリクエストする", () => { it("Scenario: 5. 新しい予定が作成されること", async () => { // Given: 有効な予定情報が用意されている const app = createTestApp(); const payload = createCalendarEventInput(); // When: 予定作成APIへPOSTリクエストを送信する const response = await request(app).post("/api/events").send(payload); // Then: ステータス201と作成された予定が返却されること expect(response.status).toBe(201); expect(response.body.event).toMatchObject({ id: 1, ...payload, }); }); }); }); |
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の重要な振る舞いを軽く確認できる形にしておくと、実装を変えるときの不安を減らすことができると思いました。
エコモットでは、モノづくりに共感してくれる仲間を募集中です!弊社に少しでも興味がある方、ぜひ下記の採用ページをご覧ください!





