今回やること
タスクを扱う小さなExpress APIを作り、Supertestで次を確認します。
GET /tasksがstatus200と空配列を返すPOST /tasksがstatus201と作成したJSONを返す- titleがないPOSTがstatus
400とエラーJSONを返す
SupertestへExpress appを直接渡すため、自分でportを開く必要はありません。テスト実行にはVitest 4を使います。
前提条件
- Node.js 20以上
- npm
- ExpressのrouteとJSON responseが分かる
プロジェクトを準備する
mkdir express-api-test
cd express-api-test
npm init -y
npm install express
npm install --save-dev vitest@4 supertest typescript \
@types/node @types/express @types/supertest
mkdir src
package.jsonへESM設定とスクリプトを追加します。依存関係はnpm installが作った内容を残してください。
{
"type": "module",
"scripts": {
"test": "vitest run",
"check": "tsc"
}
}
tsconfig.jsonを作ります。
{
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "bundler",
"target": "ES2022",
"strict": true,
"esModuleInterop": true,
"noEmit": true
},
"include": ["src"]
}
Express appを作る
src/app.tsを作ります。
import express from "express";
type Task = {
id: number;
title: string;
};
export function createApp() {
const app = express();
const tasks: Task[] = [];
app.use(express.json());
app.get("/tasks", (_request, response) => {
response.status(200).json(tasks);
});
app.post("/tasks", (request, response) => {
const title =
typeof request.body.title === "string" ? request.body.title.trim() : "";
if (title.length === 0) {
response.status(400).json({ error: "title is required" });
return;
}
const task: Task = {
id: tasks.length + 1,
title,
};
tasks.push(task);
response.status(201).json(task);
});
return app;
}
tasksをmodule全体ではなくcreateAppの内側に置きます。呼び出すたびに空の配列を持つ新しいappができるため、テスト同士でデータを共有しません。
GETとPOSTをテストする
src/app.test.tsを作ります。
import request from "supertest";
import { describe, expect, it } from "vitest";
import { createApp } from "./app";
describe("tasks API", () => {
it("GET /tasksは空配列を返す", async () => {
const app = createApp();
const response = await request(app).get("/tasks");
expect(response.status).toBe(200);
expect(response.body).toEqual([]);
});
it("POST /tasksはtaskを作る", async () => {
const app = createApp();
const response = await request(app)
.post("/tasks")
.send({ title: "Supertestを学ぶ" });
expect(response.status).toBe(201);
expect(response.body).toEqual({
id: 1,
title: "Supertestを学ぶ",
});
});
it("POST /tasksはtitleなしを拒否する", async () => {
const app = createApp();
const response = await request(app).post("/tasks").send({});
expect(response.status).toBe(400);
expect(response.body).toEqual({
error: "title is required",
});
});
});
各testの中でcreateApp()を呼んでいる点が重要です。POSTテストがtaskを追加しても、GETテストのappは別の空配列を持ちます。テストの実行順に依存しません。
成功を確認する
最初に型を確認します。
npm run check
次にテストを実行します。
npm test
3件すべてが成功すれば完了です。
Test Files 1 passed
Tests 3 passed
Supertestはappがlistenしていない場合、一時的なportへ内部でbindします。そのためapp.listen(...)やserverの終了処理は不要です。
よくあるエラー
request is not a function
esModuleInterop: trueを設定し、import request from "supertest"と書いているか確認します。
POSTのrequest.bodyがundefinedになる
routeより前にapp.use(express.json())を登録します。また、Supertestでは.send({ title: "..." })へオブジェクトを渡します。
GETテストが空配列にならない
appやtasksを全testで共有している可能性があります。各testの中でconst app = createApp()を実行してください。
練習
POSTした後に同じappへGETし、作成したtaskが配列に含まれることを1つのtestで確認してください。そのtestの中だけで同じappを2回のrequestへ渡します。
次のステップ
純粋関数やmockの基本はVitest 4入門で確認できます。ブラウザからAPIまで通すテストはPlaywright E2Eテストで別に扱います。
参考リソース
(最終確認: 2026年7月25日)
← 一覧に戻る