404 Not Found

404 Not Found


nginx

「総合プロジェクト:タスク管理API(第2部)— テスト、ドキュメント作成、およびデプロイ」

1. 仕上げと公開:アリスの3日目

3日目、アリスはテストとAPIドキュメントを作成し、ボブはDockerのデプロイ環境をセットアップした。「コードが動くからといって、デプロイの準備が整っているとは限らない」とボブは言った。「テストは品質を保証し、ドキュメントは保守性を保証し、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 })
  });
};

  1. 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() });
});

  1. CI/CDプロセス
100%
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イメージをビルド → リポジトリにプッシュ → サーバーにデプロイ。

📖 まとめ

📝 練習問題

  1. タスクの認証およびCRUD操作を網羅するテストケースを少なくとも5つ作成し、npm testを実行して、すべてが正常に実行されることを確認してください。
  2. すべてのルートにSwaggerアノテーションを追加します。アプリケーションを起動した後、/api-docs にアクセスしてドキュメントを確認してください。
  3. Dockerfile および docker-compose.yml ファイルを作成し、docker-compose up --build を実行してデプロイを確認します。
  4. /api/health ヘルスチェックエンドポイントを追加し、Docker で HEALTHCHECK ディレクティブを設定します。
  5. エラー応答の形式を統一するため、すべての throw new Error() をカスタムクラス AppError に置き換えてください。

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%