NestJS E2Eテスト(Jest + Supertest)
Supertest でHTTPリクエストを実際に送り、認証フローを含むE2Eテストの実践パターンを学ぶ
最終更新:2026/06/08
NestJSのユニットテストでServiceとControllerの単体テストを実装している前提。
この記事では、実際にNestアプリを起動してHTTPリクエストを送るE2Eテストを体系的に実装する。
この記事でやること
- テスト用のNestアプリをセットアップする(
main.tsと同じ設定を再現) GET /users/POST /usersのE2Eテストを実装する- JWT認証が必要なエンドポイントのE2Eテストを実装する
- テストデータのクリーンアップを実装する
.env.testでテスト専用DBを分離する
触るファイル一覧
api-sample/
├─ test/
│ ├─ jest-e2e.json # E2E用Jest設定
│ ├─ users.e2e-spec.ts # ユーザー管理のE2Eテスト
│ └─ auth.e2e-spec.ts # 認証フローのE2Eテスト
└─ .env.test # テスト用DB設定0. テスト用DBを分離する
E2Eテストは実際にDBを操作するため、開発用DBとは別のDBを用意する。
# .env.test
DATABASE_URL="file:./test.db".env.test をCIや他の開発者が見つけやすいように、.env.example にも記載しておく。
# package.json の test:e2e スクリプトで .env.test を読む
"test:e2e": "dotenv -e .env.test -- jest --config ./test/jest-e2e.json"dotenv-cli がない場合はインストールする。
pnpm add -D dotenv-cli
# npm の場合
npm install -D dotenv-cliテスト前にマイグレーションを適用する。
dotenv -e .env.test -- pnpm exec prisma migrate deploy1. E2E用Jestの設定を確認する
test/jest-e2e.json の基本設定。
{
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": ".",
"testEnvironment": "node",
"testRegex": ".e2e-spec.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"moduleNameMapper": {
"^src/(.*)$": "<rootDir>/../src/$1",
"^(\\.{1,2}/.*)\\.js$": "$1"
}
}Prisma 7で dynamic import エラーが出る場合は、package.json の test:e2e スクリプトに NODE_OPTIONS=--experimental-vm-modules を追加する(詳細はNestJSテスト入門を参照)。
2. users.e2e-spec.ts を実装する
// =============================================================================
// ファイル: test/users.e2e-spec.ts
// 実行: pnpm run test:e2e
// 中身: AppModule を起動し、/users の CRUD を HTTP リクエストで確認する
// =============================================================================
import { INestApplication, ValidationPipe } from "@nestjs/common";
import { Test, TestingModule } from "@nestjs/testing";
import request from "supertest";
import { AppModule } from "../src/app.module";
import { GlobalExceptionFilter } from "../src/common/filters/http-exception.filter";
import { PrismaService } from "../src/prisma/prisma.service";
describe("Users (e2e)", () => {
let app: INestApplication;
let prisma: PrismaService;
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
// main.ts と同じ設定を再現する(ここがズレると E2E だけ挙動が違う)
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
}),
);
app.useGlobalFilters(new GlobalExceptionFilter());
await app.init();
prisma = moduleFixture.get(PrismaService);
});
afterAll(async () => {
// テストで作ったデータを削除してからアプリを閉じる
await prisma.user.deleteMany({
where: { email: { contains: "@e2e-test.example.com" } },
});
await app.close();
});
// ─────────────────────────────────────────────────────────────────────────
// GET /users
// ─────────────────────────────────────────────────────────────────────────
describe("GET /users", () => {
it("200 と配列を返す", () => {
return request(app.getHttpServer())
.get("/users")
.expect(200)
.expect((res) => {
expect(Array.isArray(res.body)).toBe(true);
});
});
});
// ─────────────────────────────────────────────────────────────────────────
// POST /users
// ─────────────────────────────────────────────────────────────────────────
describe("POST /users", () => {
it("201 とユーザーを返す", async () => {
const email = `create-${Date.now()}@e2e-test.example.com`;
const res = await request(app.getHttpServer())
.post("/users")
.send({ email, name: "E2E User", password: "password123" })
.expect(201);
expect(res.body).toMatchObject({ email, name: "E2E User" });
// パスワードはレスポンスに含まれない
expect(res.body.passwordHash).toBeUndefined();
expect(res.body.password).toBeUndefined();
});
it("email が空のとき 400 を返す", () => {
return request(app.getHttpServer())
.post("/users")
.send({ name: "No Email", password: "password123" })
.expect(400);
});
it("password が短すぎるとき 400 を返す", () => {
return request(app.getHttpServer())
.post("/users")
.send({ email: `short-${Date.now()}@e2e-test.example.com`, name: "Short", password: "123" })
.expect(400);
});
it("定義外フィールドを送ると 400 を返す(forbidNonWhitelisted)", () => {
return request(app.getHttpServer())
.post("/users")
.send({
email: `extra-${Date.now()}@e2e-test.example.com`,
name: "Extra",
password: "password123",
role: "admin", // DTO に無いフィールド
})
.expect(400);
});
it("メールアドレスが重複しているとき 400 を返す", async () => {
const email = `dup-${Date.now()}@e2e-test.example.com`;
// 1回目は成功
await request(app.getHttpServer())
.post("/users")
.send({ email, name: "First", password: "password123" })
.expect(201);
// 2回目は失敗
await request(app.getHttpServer())
.post("/users")
.send({ email, name: "Second", password: "password123" })
.expect(400);
});
});
// ─────────────────────────────────────────────────────────────────────────
// GET /users/:id
// ─────────────────────────────────────────────────────────────────────────
describe("GET /users/:id", () => {
it("存在するユーザーを返す", async () => {
// まずユーザーを作る
const email = `getone-${Date.now()}@e2e-test.example.com`;
const createRes = await request(app.getHttpServer())
.post("/users")
.send({ email, name: "GetOne", password: "password123" })
.expect(201);
const userId = createRes.body.id as string;
// IDで取得する
const res = await request(app.getHttpServer())
.get(`/users/${userId}`)
.expect(200);
expect(res.body).toMatchObject({ id: userId, email });
});
it("存在しない ID のとき 404 を返す", () => {
return request(app.getHttpServer())
.get("/users/nonexistent-id-12345")
.expect(404);
});
});
});3. auth.e2e-spec.ts でJWT認証をテストする
認証が必要なエンドポイントは、ログインしてトークンを取得してから叩く。
// =============================================================================
// ファイル: test/auth.e2e-spec.ts
// 実行: pnpm run test:e2e
// 中身: ログイン → JWT 取得 → 認証必須エンドポイントを叩く
// =============================================================================
import { INestApplication, ValidationPipe } from "@nestjs/common";
import { Test, TestingModule } from "@nestjs/testing";
import request from "supertest";
import { AppModule } from "../src/app.module";
import { GlobalExceptionFilter } from "../src/common/filters/http-exception.filter";
import { PrismaService } from "../src/prisma/prisma.service";
describe("Auth (e2e)", () => {
let app: INestApplication;
let prisma: PrismaService;
const testEmail = `auth-${Date.now()}@e2e-test.example.com`;
const testPassword = "password123";
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
app.useGlobalPipes(
new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true }),
);
app.useGlobalFilters(new GlobalExceptionFilter());
await app.init();
prisma = moduleFixture.get(PrismaService);
// テスト用ユーザーを作る
await request(app.getHttpServer())
.post("/users")
.send({ email: testEmail, name: "Auth Tester", password: testPassword });
});
afterAll(async () => {
await prisma.user.deleteMany({
where: { email: { contains: "@e2e-test.example.com" } },
});
await app.close();
});
// ─────────────────────────────────────────────────────────────────────────
// POST /auth/login
// ─────────────────────────────────────────────────────────────────────────
describe("POST /auth/login", () => {
it("正しい認証情報でログインすると accessToken を返す", async () => {
const res = await request(app.getHttpServer())
.post("/auth/login")
.send({ email: testEmail, password: testPassword })
.expect(200);
expect(res.body.accessToken).toBeDefined();
expect(typeof res.body.accessToken).toBe("string");
});
it("パスワードが間違っているとき 401 を返す", () => {
return request(app.getHttpServer())
.post("/auth/login")
.send({ email: testEmail, password: "wrong-password" })
.expect(401);
});
it("存在しないユーザーのとき 401 を返す", () => {
return request(app.getHttpServer())
.post("/auth/login")
.send({ email: "no-user@example.com", password: "any" })
.expect(401);
});
});
// ─────────────────────────────────────────────────────────────────────────
// GET /auth/me(JWT認証必須)
// ─────────────────────────────────────────────────────────────────────────
describe("GET /auth/me", () => {
let accessToken: string;
beforeAll(async () => {
const res = await request(app.getHttpServer())
.post("/auth/login")
.send({ email: testEmail, password: testPassword });
accessToken = res.body.accessToken as string;
});
it("有効なトークンで自分のプロフィールを返す", async () => {
const res = await request(app.getHttpServer())
.get("/auth/me")
.set("Authorization", `Bearer ${accessToken}`)
.expect(200);
expect(res.body).toMatchObject({ email: testEmail });
expect(res.body.passwordHash).toBeUndefined();
});
it("Authorizationヘッダー無しのとき 401 を返す", () => {
return request(app.getHttpServer())
.get("/auth/me")
.expect(401);
});
it("不正なトークンのとき 401 を返す", () => {
return request(app.getHttpServer())
.get("/auth/me")
.set("Authorization", "Bearer invalid-token-here")
.expect(401);
});
});
});4. E2Eテストを実行する
pnpm run test:e2ePASS test/users.e2e-spec.ts
Users (e2e)
GET /users
✓ 200 と配列を返す (53 ms)
POST /users
✓ 201 とユーザーを返す (42 ms)
✓ email が空のとき 400 を返す (8 ms)
✓ password が短すぎるとき 400 を返す (7 ms)
✓ 定義外フィールドを送ると 400 を返す (6 ms)
✓ メールアドレスが重複しているとき 400 を返す (31 ms)
GET /users/:id
✓ 存在するユーザーを返す (38 ms)
✓ 存在しない ID のとき 404 を返す (9 ms)
PASS test/auth.e2e-spec.ts
...実装タスク(チェックリスト)
-
.env.testを作成してテスト用DBを分離 -
dotenv-cliをインストールしてtest:e2eスクリプトを更新 -
test/users.e2e-spec.tsを作成(CRUD + バリデーションエラー) -
test/auth.e2e-spec.tsを作成(ログイン + 認証必須エンドポイント) -
pnpm run test:e2eが全てPASSする状態にする -
afterAllでテストデータを削除していることを確認する