「総合プロジェクト:タスク管理API(第2部)— テスト、ドキュメント作成、およびデプロイ」
1. 仕上げと公開:アリスの3日目
3日目、アリスはテストとAPIドキュメントを作成し、ボブはDockerのデプロイ環境をセットアップした。「コードが動くからといって、デプロイの準備が整っているとは限らない」とボブは言った。「テストは品質を保証し、ドキュメントは保守性を保証し、Dockerは一貫性を保証するのだ。」
- Node.js APIのテストには、JestとSupertestの組み合わせが標準となっています
- 一元化されたエラー処理により、APIの応答形式の一貫性が確保されます
- Swaggerはドキュメントを自動的に生成します。コードそのものがドキュメントとなります。
- Dockerによるコンテナ化により、開発環境と本番環境間の一貫性が確保されます
- ヘルスチェックは、本番環境へのデプロイにおいて必須の設定です
2. JestとSupertestを使ったテスト
(1) ファイル構造の確認
| ファイル | 説明 |
|---|---|
tests/setup.js |
テスト環境の設定(テストデータベースへの接続) |
tests/auth.test.js |
認証モジュールのテスト |
tests/tasks.test.js |
タスクモジュールのテスト |
tests/helpers.js |
テスト用ヘルパー関数(テストユーザーの作成など) |
▶ 例:テスト環境の設定
JAVASCRIPT
// tests/setup.js
process.env.JWT_SECRET = 'test-secret';
process.env.MONGO_URI = 'mongodb://localhost:27017/task-manager-test';
const mongoose = require('mongoose');
beforeAll(async () => await mongoose.connect(process.env.MONGO_URI));
afterAll(async () => {
await mongoose.connection.dropDatabase();
await mongoose.connection.close();
});
▶ 例:認証モジュールのテスト
JAVASCRIPT
const request = require('supertest');
const app = require('../src/app');
const User = require('../src/models/User');
describe('Auth API', () => {
beforeEach(async () => await User.deleteMany({}));
test('should register a new user', async () => {
const res = await request(app).post('/api/auth/register').send({
username: 'alice', email: 'alice@test.com', password: '123456'
});
expect(res.status).toBe(201);
expect(res.body.token).toBeDefined();
expect(res.body.user.username).toBe('alice');
});
test('should login existing user', async () => {
await request(app).post('/api/auth/register').send({
username: 'alice', email: 'alice@test.com', password: '123456'
});
const res = await request(app).post('/api/auth/login').send({
email: 'alice@test.com', password: '123456'
});
expect(res.status).toBe(200);
expect(res.body.token).toBeDefined();
});
test('should reject invalid credentials', async () => {
const res = await request(app).post('/api/auth/login').send({
email: 'noone@test.com', password: 'wrong'
});
expect(res.status).toBe(401);
});
});
▶ 例:タスクモジュールのテスト
JAVASCRIPT
const request = require('supertest');
const app = require('../src/app');
const Task = require('../src/models/Task');
const User = require('../src/models/User');
let token, userId;
beforeEach(async () => {
await User.deleteMany({});
await Task.deleteMany({});
const reg = await request(app).post('/api/auth/register').send({
username: 'bob', email: 'bob@test.com', password: '123456'
});
token = reg.body.token;
userId = reg.body.user.id;
});
test('should create a task', async () => {
const res = await request(app).post('/api/tasks').set('Authorization', `Bearer ${token}`).send({ title: 'Write tests' });
expect(res.status).toBe(201);
expect(res.body.title).toBe('Write tests');
});
test('should get tasks list', async () => {
await Task.create({ title: 'Task1', assignedTo: userId });
const res = await request(app).get('/api/tasks').set('Authorization', `Bearer ${token}`);
expect(res.status).toBe(200);
expect(res.body.tasks.length).toBe(1);
});
test('should deny access without token', async () => {
const res = await request(app).get('/api/tasks');
expect(res.status).toBe(401);
});
3. エラー処理の統一的なカプセル化
▶ 例:カスタムエラークラス
JAVASCRIPT
class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
this.isOperational = true;
}
}
module.exports = AppError;
▶ 例:グローバルエラー処理ミドルウェア
JAVASCRIPT
module.exports = (err, req, res, next) => {
const statusCode = err.statusCode || 500;
const message = err.isOperational ? err.message : 'Internal Server Error';
res.status(statusCode).json({
status: statusCode >= 400 && statusCode < 500 ? 'fail' : 'error',
message,
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
});
};
- Swagger API ドキュメント
(1) 一般的な Swagger アノテーションタグ
| タグ | 説明 | 使い方 |
|---|---|---|
@openapi |
パスおよび操作の定義 | ルーティングファイル |
@swagger |
コンポーネント定義(スキーマ) | ドキュメント設定 |
@tags |
APIグループ | ルーティングファイル |
@security |
認証方法のリファレンス | 認証が必要なエンドポイント |
@produces |
応答形式 | ルーティングファイル |
@parameters |
リクエストパラメータ | ルートファイル |
▶ 例:Swaggerの設定
JAVASCRIPT
const swaggerJsdoc = require('swagger-jsdoc');
const swaggerUi = require('swagger-ui-express');
const options = {
definition: {
openapi: '3.0.0',
info: { title: 'Task Manager API', version: '1.0.0', description: 'Team Task Management RESTful API' },
servers: [{ url: 'http://localhost:3000/api' }],
components: {
securitySchemes: {
bearerAuth: { type: 'http', scheme: 'bearer', bearerFormat: 'JWT' }
}
}
},
apis: ['./src/routes/*.js']
};
const specs = swaggerJsdoc(options);
module.exports = { swaggerUi, specs };
▶ 例:ルーティングにおける Swagger アノテーション
JAVASCRIPT
/**
* @openapi
* /auth/register:
* post:
* tags: [Auth]
* summary: User Registration
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required: [username, email, password]
* properties:
* username: { type: string }
* email: { type: string, format: email }
* password: { type: string, minLength: 6 }
* responses:
* 201:
* description: Registration Successful
* 400:
* description: Parameter error
*/
router.post('/register', async (req, res, next) => { /* ... */ });
4. Dockerのデプロイ
(1) Dockerfile の説明
| ファイル | 説明 |
|---|---|
Dockerfile |
Node.js アプリケーションイメージの構築 |
docker-compose.yml |
オーケストレーション・アプリケーション + MongoDB コンテナ |
.dockerignore |
node_modules などの不要なファイルを除外する |
(2) プロジェクト完了チェックリスト
| 項目 | ステータス |
|---|---|
| プロジェクトは通常通り開始できます | ☐ |
| 登録・ログイン API 利用可能 | ☐ |
| タスクのCRUD操作が可能 | ☐ |
| ページネーションとフィルタリングが可能 | ☐ |
| アクセス制御は正常に機能しています | ☐ |
| テスト合格 | ☐ |
| Swaggerのドキュメントにアクセスできる | ☐ |
| Dockerのビルドに成功しました | ☐ |
| ヘルスチェックのエンドポイント:正常 | ☐ |
| 一元化されたエラー処理 | ☐ |
例:Dockerfile
DOCKERFILE
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3000
HEALTHCHECK --interval=30s CMD wget -qO- http://localhost:3000/api/health || exit 1
CMD ["node", "server.js"]
▶ 例:docker-compose.yml
YAML
version: '3.8'
services:
app:
build: .
ports:
- "3000:3000"
environment:
- MONGO_URI=mongodb://mongo:27017/task-manager
- JWT_SECRET=${JWT_SECRET}
depends_on:
- mongo
restart: unless-stopped
mongo:
image: mongo:7
volumes:
- mongo-data:/data/db
ports:
- "27017:27017"
volumes:
mongo-data:
▶ 例:ヘルスチェックエンドポイント
JAVASCRIPT
router.get('/health', (req, res) => {
res.json({ status: 'ok', uptime: process.uptime(), timestamp: new Date().toISOString() });
});
- CI/CDプロセス
graph LR
A[Code Push] --> B[Run Jest Test]
B -->|Through| C[Build Docker Image]
B -->|Failure| D[Notice to Developers]
C --> E[Push the image to Registry]
E --> F[Deploy to the server]
F --> G[Health Checkup]
G -->|Through| H[Deployment Complete]
G -->|Failure| I[Rollback to a Previous Version]
▶ 例:プロジェクト起動コマンドの概要
BASH
# Development Environment
npm run dev
# Run Test
npm test
# Docker Build
docker-compose up --build
# Production Deployment
docker-compose -f docker-compose.prod.yml up -d
5. 包括的な例:テスト + ドキュメント + Docker の完全な設定
JAVASCRIPT
// tests/tasks.test.js
const request = require('supertest');
const app = require('../src/app');
const Task = require('../src/models/Task');
const User = require('../src/models/User');
let token, userId;
beforeEach(async () => {
await User.deleteMany({});
await Task.deleteMany({});
const reg = await request(app).post('/api/auth/register')
.send({ username: 'testuser', email: 'test@test.com', password: '123456' });
token = reg.body.token;
userId = reg.body.user.id;
});
describe('Task API', () => {
test('POST /api/tasks - create task', async () => {
const res = await request(app).post('/api/tasks')
.set('Authorization', `Bearer ${token}`)
.send({ title: 'My Task', priority: 'high' });
expect(res.status).toBe(201);
});
test('GET /api/tasks - list with pagination', async () => {
for (let i = 0; i < 15; i++) {
await Task.create({ title: `Task ${i}`, assignedTo: userId });
}
const res = await request(app).get('/api/tasks?page=2&limit=5')
.set('Authorization', `Bearer ${token}`);
expect(res.status).toBe(200);
expect(res.body.tasks.length).toBe(5);
expect(res.body.page).toBe(2);
});
test('DELETE /api/tasks/:id - owner can delete', async () => {
const task = await Task.create({ title: 'To Delete', assignedTo: userId });
const res = await request(app).delete(`/api/tasks/${task._id}`)
.set('Authorization', `Bearer ${token}`);
expect(res.status).toBe(200);
});
});
JAVASCRIPT
// src/config/swagger.js
const swaggerJsdoc = require('swagger-jsdoc');
const swaggerUi = require('swagger-ui-express');
const specs = swaggerJsdoc({
definition: {
openapi: '3.0.0',
info: { title: 'Task Manager API', version: '1.0.0' },
servers: [{ url: '/api' }],
components: { securitySchemes: { bearerAuth: { type: 'http', scheme: 'bearer' } } }
},
apis: ['./src/routes/*.js']
});
module.exports = { swaggerUi, specs };
DOCKERFILE
# Dockerfile
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3000
HEALTHCHECK --interval=30s CMD wget -qO- http://localhost:3000/api/health || exit 1
CMD ["node", "server.js"]
YAML
# docker-compose.yml
version: '3.8'
services:
app:
build: .
ports: ["3000:3000"]
environment:
- MONGO_URI=mongodb://mongo:27017/task-manager
- JWT_SECRET=change_me_in_prod
depends_on: [mongo]
mongo:
image: mongo:7
volumes: [mongo-data:/data/db]
volumes:
mongo-data:
❓ よくある質問
Q JestとMochaのどちらを選べばいいですか?
A Jestはインストールするだけですぐに使用でき、設定も不要で、アサーションやコードカバレッジ機能が組み込まれています。一方、Mochaはより柔軟性がありますが、chaiやsinonが必要です。Node.jsのAPIテストには、JestとSupertestの組み合わせをお勧めします。
Q Supertestで認証が必要なエンドポイントをテストするにはどうすればよいですか?
A まず、ログインエンドポイントを呼び出してトークンを取得し、その後、.set('Authorization', 'Bearer ' + token) を使用して、そのトークンを以降のリクエストに渡します。
Q Swaggerのドキュメントは手動で作成する必要がありますか?
A
swagger-jsdoc を使用すれば、JSDocのコメントから自動的に生成できますし、swagger-ui-express を使用して表示することもできます。コメント自体がドキュメントの役割を果たすため、メンテナンスの手間は最小限で済みます。Q Docker イメージのサイズを最適化するにはどうすればよいですか?
A Alpine ベースイメージを使用し、マルチステージビルド(ビルド段階で依存関係をインストールし、実行時にはアーティファクトのみをコピーする)を採用し、
.dockerignore ファイルを使用して不要なファイルを除外してください。Q CI/CDパイプラインはどのように設定すればよいですか?
A GitHubプロジェクトの場合は、GitHub Actionsを使用します。プッシュをトリガーとして → 依存関係をインストール → テストを実行 → Dockerイメージをビルド → リポジトリにプッシュ → サーバーにデプロイ。
- Q: なぜAPIドキュメントを自動生成すべきなのでしょうか? A: ドキュメントを手作業で管理していると、コードとの同期が簡単にずれてしまいます。Swaggerアノテーションはコード内に直接記述されるため、コードを変更するとドキュメントが自動的に更新され、一貫性が保たれます。
- Q: テストのカバレッジはどの程度にするべきですか? A: コアとなるビジネスロジックについては、登録、ログイン、CRUD、アクセス制御などの主要な処理フローやエッジケースを含め、少なくとも80%をカバーすることをお勧めします。
- Q: ベアメタル展開に比べて、Dockerによる展開にはどのような利点がありますか? A: 環境の一貫性(「自分のマシンでは動く」という問題を解消)、迅速な展開、リソースの分離、CI/CDとの統合の容易さ、そして水平スケーリングです。
- Q: 本番環境ではどのような点に注意すべきですか? A: JWT_SECRET には強固なシークレットを使用し、MongoDB で認証を有効にし、HTTPS を有効にし、CORS のオリジンを制限し、レート制限を設定し、ログ収集を構成してください。
- Q: ヘルスチェックはどのように実行しますか? A: アプリケーションとデータベースのステータスを返すために、
/api/healthエンドポイントを指定してください。DockerのHEALTHCHECKまたはKubernetesのlivenessProbeが、このエンドポイントを定期的に呼び出します。 - Q: テスト用データベースは本番用データベースと競合しますか? A: テストでは別のデータベース(
task-manager-testなど)を使用しており、各テストスイートの実施前後にデータがクリーンアップされるため、本番環境に影響を与えることはありません。
📖 まとめ
- まとめと開始:3日目のアリスの基本概念と使い方
- テストにおけるJestとSupertestの基本概念と使い方
- 統一されたエラー処理のカプセル化に関する基本概念と使用方法
- Swagger APIドキュメントの基本概念と使い方
- Dockerデプロイメントの基本概念と使い方
- CI/CD ワークフローの基本概念とベストプラクティス
- 包括的な事例:テストの核心的な概念と活用法 + ドキュメント作成 + Dockerの完全な設定
📝 練習問題
- タスクの認証およびCRUD操作を網羅するテストケースを少なくとも5つ作成し、
npm testを実行して、すべてが正常に実行されることを確認してください。 - すべてのルートにSwaggerアノテーションを追加します。アプリケーションを起動した後、
/api-docsにアクセスしてドキュメントを確認してください。 - Dockerfile および docker-compose.yml ファイルを作成し、
docker-compose up --buildを実行してデプロイを確認します。 /api/healthヘルスチェックエンドポイントを追加し、Docker で HEALTHCHECK ディレクティブを設定します。- エラー応答の形式を統一するため、すべての
throw new Error()をカスタムクラスAppErrorに置き換えてください。



