Express APIをSupertestでテストする - GET・POSTのstatusとbody

初級 | 20分 で読める | 2025.12.20

公式ドキュメント

今回やること

タスクを扱う小さなExpress APIを作り、Supertestで次を確認します。

  • GET /tasksがstatus 200と空配列を返す
  • POST /tasksがstatus 201と作成した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.bodyundefinedになる

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日)

← 一覧に戻る
PR
PR
PR
PR