Node.js: テストとデバッグ

最終更新:2026-08-26

ボブはECサイト向けにRESTful APIを開発していたが、新機能を追加するたびに別のどこかが壊れていた。Postmanによる手動テストは1回のデプロイごとに30分もかかり、それでも境界ケースを見逃していた。彼はユニットテストにJest、API統合テストにSupertestを導入した。今ではnpm testで50件のテストが3秒で実行され、すべてのデプロイは自動化されたCIチェックに支えられている。さらに構造化ロギングにwinstonを追加したため、バグがすり抜けてもログが正確に原因を教えてくれる。

このレッスンで学ぶこと:


1. テストの概要

(1) なぜテストを書くのか

テストなし テストあり
手動検証、毎回30分 自動化、3秒
1つ変えるとすべて壊れる 1つ変えても、テストが何が壊れたか教えてくれる
デプロイ前の不安 グリーンライト、自信を持ってデプロイ
リファクタリングが怖い テストという安全網の上でリファクタリング

▶ サンプル:(2) テストピラミッド

100%
graph TD
    A["Unit Tests<br/>Many / Fast / Cheap"] --> B["Integration Tests<br/>Moderate / Slower"]
    B --> C["E2E Tests<br/>Few / Slow / Expensive"]
    style A fill:#4CAF50,color:#fff
    style B fill:#FF9800,color:#fff
    style C fill:#F44336,color:#fff

(3) Node.jsテストツールの比較

ツール 種別 特徴 最適な用途
Jest ユニット + 統合 ゼロ設定、モック/カバレッジ内蔵、スナップショットテスト 汎用
Mocha ユニット + 統合 柔軟、プラグイン豊富、chai/sinonが必要 カスタム要件
Vitest ユニット Viteエコシステム、ネイティブESM、非常に高速 Viteプロジェクト
node:test ユニット Node.js内蔵、依存ゼロ シンプルなプロジェクト
Supertest HTTPテスト HTTPリクエストのシミュレーション、Expressルートのテスト API統合テスト
Playwright E2E ブラウザ自動化、マルチブラウザ対応 フロントエンドE2E

このレッスンでは、Node.jsコミュニティで最も主流なテスト手法であるJest + Supertestの組み合わせを使用します。



2. Jestによるユニットテスト

▶ サンプル:(1) インストールとセットアップ

BASH
npm install --save-dev jest

package.jsonにテストスクリプトを追加します:

JSON
{
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage"
  }
}

Jestはデフォルトでゼロ設定で動作し、*.test.jsまたは*.spec.jsファイルを自動的に検出します。

▶ サンプル:(2) 基本構文:describe / it / expect

JAVASCRIPT
// math.js — テスト対象のモジュール
function add(a, b) {
  return a + b;
}

function subtract(a, b) {
  return a - b;
}

function multiply(a, b) {
  return a * b;
}

function divide(a, b) {
  if (b === 0) throw new Error('Division by zero');
  return a / b;
}

module.exports = { add, subtract, multiply, divide };
▶ 試してみよう
JAVASCRIPT
// math.test.js
const { add, subtract, multiply, divide } = require('./math');

describe('Math utilities', () => {
  test('add should sum two numbers', () => {
    expect(add(2, 3)).toBe(5);
    expect(add(-1, 1)).toBe(0);
  });

  test('subtract should return the difference', () => {
    expect(subtract(5, 3)).toBe(2);
  });

  test('multiply should return the product', () => {
    expect(multiply(3, 4)).toBe(12);
  });

  test('divide should throw on zero divisor', () => {
    expect(() => divide(10, 0)).toThrow('Division by zero');
  });
});

実行:

BASH
npm test

Output:

TEXT 📖 参照専用
 PASS  ./math.test.js
  Math utilities
    ✓ add should sum two numbers (2 ms)
    ✓ subtract should return the difference
    ✓ multiply should return the product
    ✓ divide should throw on zero divisor (1 ms)

Tests:       4 passed, 4 total
Time:        0.5s

(3) よく使うマッチャー

マッチャー 意味
toBe 厳密等価(===) expect(1 + 1).toBe(2)
toEqual 深い等価 expect(obj).toEqual({ a: 1 })
toBeTruthy / toBeFalsy 真偽チェック expect(flag).toBeTruthy()
toBeNull / toBeUndefined null / undefined expect(val).toBeNull()
toThrow 例外をスロー expect(fn).toThrow()
toContain 配列に含まれる expect([1, 2, 3]).toContain(2)
toMatch 正規表現一致 expect('hello').toMatch(/ell/)
toHaveLength 長さチェック expect('abc').toHaveLength(3)
resolves / rejects Promiseの結果 expect(promise).resolves.toBe(5)
toHaveBeenCalled モックが呼び出された expect(fn).toHaveBeenCalled()

▶ サンプル:(4) 非同期テスト

JAVASCRIPT
// async-functions.js
function fetchUser(id) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({ id, name: `User_${id}` });
    }, 100);
  });
}

async function getUserEmail(id) {
  const user = await fetchUser(id);
  return `${user.name.toLowerCase()}@example.com`;
}

module.exports = { fetchUser, getUserEmail };
▶ 試してみよう
JAVASCRIPT
// async-functions.test.js
const { fetchUser, getUserEmail } = require('./async-functions');

describe('Async function tests', () => {
  test('fetchUser returns user object', async () => {
    const user = await fetchUser(1);
    expect(user).toEqual({ id: 1, name: 'User_1' });
  });

  test('getUserEmail returns formatted email', async () => {
    const email = await getUserEmail(42);
    expect(email).toBe('user_42@example.com');
  });

  test('fetchUser resolves within 200ms', async () => {
    await expect(fetchUser(1)).resolves.toHaveProperty('id', 1);
  });
});

(5) モッキング

モックは外部依存(データベース、HTTPリクエスト、ファイルシステム)を隔離し、テストが現在のモジュールのロジックだけに集中できるようにします。

JAVASCRIPT
// email-service.js
class EmailService {
  send(to, subject, body) {
    // 実際のSMTP呼び出し — ユニットテストには遅すぎる
    console.log(`Sending email to ${to}: ${subject}`);
    return true;
  }
}

class UserService {
  constructor(emailService) {
    this.emailService = emailService;
  }

  register(name, email) {
    // ビジネスロジック:検証 + 歓迎メールの送信
    if (!email.includes('@')) throw new Error('Invalid email');
    this.emailService.send(email, 'Welcome', `Hello ${name}!`);
    return { name, email, status: 'registered' };
  }
}

module.exports = { EmailService, UserService };
JAVASCRIPT
// email-service.test.js
const { EmailService, UserService } = require('./email-service');

describe('UserService', () => {
  let mockEmailService;
  let userService;

  beforeEach(() => {
    mockEmailService = {
      send: jest.fn().mockReturnValue(true),
    };
    userService = new UserService(mockEmailService);
  });

  test('register should call emailService.send', () => {
    const result = userService.register('Alice', 'alice@example.com');

    expect(result).toEqual({
      name: 'Alice',
      email: 'alice@example.com',
      status: 'registered',
    });

    expect(mockEmailService.send).toHaveBeenCalledWith(
      'alice@example.com',
      'Welcome',
      'Hello Alice!'
    );
  });

  test('register should throw on invalid email', () => {
    expect(() => userService.register('Bob', 'invalid')).toThrow('Invalid email');
    expect(mockEmailService.send).not.toHaveBeenCalled();
  });

  test('register should send exactly one email', () => {
    userService.register('Charlie', 'charlie@example.com');
    expect(mockEmailService.send).toHaveBeenCalledTimes(1);
  });
});

▶ サンプル:Jestの基本アサーション

JAVASCRIPT
test('basic matchers', () => {
  expect(2 + 2).toBe(4);
  expect({ name: 'Alice' }).toEqual({ name: 'Alice' });
  expect([1, 2, 3]).toContain(2);
  expect('hello world').toMatch(/world/);
  expect(() => { throw new Error('fail'); }).toThrow('fail');
});
▶ 試してみよう

よく使われるJestのマッチャーを示します:プリミティブにはtoBe、オブジェクトにはtoEqual、配列にはtoContain、文字列にはtoMatch、エラーテストにはtoThrowを使用します。




3. SupertestによるAPI統合テスト

▶ サンプル:(1) インストールと仕組み

BASH
npm install --save-dev supertest

SupertestはExpressアプリケーションをメモリ上で起動し、HTTPリクエストを送信してレスポンスを検証します。実際のポート待受は不要です。

100%
flowchart LR
    A["Test File"] -->|"request(app)"| B["Express App<br/>(in-memory)"]
    B -->|"response"| A
    A -->|"assert"| C["Jest expect"]

▶ サンプル:(2) Express APIのテスト

JAVASCRIPT
// app.js — Expressアプリ(listenせずにエクスポート)
const express = require('express');
const app = express();

app.use(express.json());

let products = [
  { id: 1, name: 'Laptop', price: 999.99 },
  { id: 2, name: 'Phone', price: 499.99 },
];

app.get('/api/products', (req, res) => {
  res.json(products);
});

app.get('/api/products/:id', (req, res) => {
  const product = products.find(p => p.id === parseInt(req.params.id));
  if (!product) return res.status(404).json({ error: 'Product not found' });
  res.json(product);
});

app.post('/api/products', (req, res) => {
  const { name, price } = req.body;
  if (!name || price == null) {
    return res.status(400).json({ error: 'Name and price are required' });
  }
  const newProduct = { id: products.length + 1, name, price };
  products.push(newProduct);
  res.status(201).json(newProduct);
});

module.exports = app;
▶ 試してみよう

補足:app.jsappをエクスポートするだけで、モジュール内でapp.listen()を呼び出しません。listenserver.jsに置くため、Supertestがappオブジェクトを直接テストできます。

JAVASCRIPT
// app.test.js
const request = require('supertest');
const app = require('./app');

describe('Products API', () => {
  test('GET /api/products should return product list', async () => {
    const response = await request(app).get('/api/products');

    expect(response.status).toBe(200);
    expect(response.body).toHaveLength(2);
    expect(response.body[0]).toHaveProperty('name', 'Laptop');
  });

  test('GET /api/products/:id should return a single product', async () => {
    const response = await request(app).get('/api/products/1');

    expect(response.status).toBe(200);
    expect(response.body.id).toBe(1);
  });

  test('GET /api/products/:id should return 404 for missing product', async () => {
    const response = await request(app).get('/api/products/999');

    expect(response.status).toBe(404);
    expect(response.body).toHaveProperty('error', 'Product not found');
  });

  test('POST /api/products should create a new product', async () => {
    const response = await request(app)
      .post('/api/products')
      .send({ name: 'Tablet', price: 299.99 });

    expect(response.status).toBe(201);
    expect(response.body).toHaveProperty('name', 'Tablet');
    expect(response.body).toHaveProperty('id', 3);
  });

  test('POST /api/products should return 400 for missing fields', async () => {
    const response = await request(app)
      .post('/api/products')
      .send({ name: 'Incomplete Product' });

    expect(response.status).toBe(400);
    expect(response.body).toHaveProperty('error');
  });
});

▶ サンプル:(3) 認証付きAPIのテスト

JAVASCRIPT
// protected-routes.test.js
const request = require('supertest');
const app = require('./app');

describe('Protected API routes', () => {
  let token;

  beforeAll(async () => {
    // ログインしてJWTトークンを取得
    const response = await request(app)
      .post('/api/auth/login')
      .send({ username: 'alice', password: 'secret123' });
    token = response.body.token;
  });

  test('should access protected route with valid token', async () => {
    const response = await request(app)
      .get('/api/profile')
      .set('Authorization', `Bearer ${token}`);

    expect(response.status).toBe(200);
    expect(response.body).toHaveProperty('username', 'alice');
  });

  test('should reject access without token', async () => {
    const response = await request(app).get('/api/profile');

    expect(response.status).toBe(401);
  });

  test('should reject access with invalid token', async () => {
    const response = await request(app)
      .get('/api/profile')
      .set('Authorization', 'Bearer invalid_token_here');

    expect(response.status).toBe(401);
  });
});
▶ 試してみよう

▶ サンプル:APIヘルスチェックテスト

JAVASCRIPT
const request = require('supertest');
const express = require('express');

const app = express();
app.get('/health', (req, res) => res.json({ status: 'ok' }));

describe('GET /health', () => {
  it('returns 200 with status ok', async () => {
    const res = await request(app).get('/health');
    expect(res.status).toBe(200);
    expect(res.body.status).toBe('ok');
  });
});
▶ 試してみよう

Supertestを使った最小限のAPIヘルスチェックテストです。基本的なExpressアプリを作成し、GETリクエストを送信して、JSONレスポンスのボディとステータスコードを検証します。




4. Node.jsデバッグ

(1) node inspect + Chrome DevTools

Node.jsには標準のデバッガが内蔵されており、追加のツールは不要です。

BASH
node inspect app.js

その後、Chromeでchrome://inspectを開き、「Open dedicated DevTools for Node」をクリックします。

よく使うデバッグ操作:

操作 DevToolsショートカット コマンドライン
続行 F8 c
ステップオーバー F10 n
ステップイン F11 s
ステップアウト Shift+F11 o
ブレークポイント設定 行番号をクリック setBreakpoint()

▶ サンプル:(2) コードにブレークポイントを設定する

JAVASCRIPT
// debug-demo.js
function calculateDiscount(price, memberLevel) {
  debugger; // ここでインスペクタの実行が一時停止する
  let discount = 0;
  if (memberLevel === 'gold') discount = 0.2;
  else if (memberLevel === 'silver') discount = 0.1;
  const finalPrice = price * (1 - discount);
  return finalPrice;
}

console.log(calculateDiscount(100, 'gold'));
console.log(calculateDiscount(100, 'silver'));
console.log(calculateDiscount(100, 'unknown'));
▶ 試してみよう
BASH
node inspect debug-demo.js

▶ サンプル:(3) consoleデバッグのヒント

JAVASCRIPT
// consoleデバッグのメソッド
const users = [
  { id: 1, name: 'Alice', role: 'admin' },
  { id: 2, name: 'Bob', role: 'user' },
  { id: 3, name: 'Charlie', role: 'admin' },
];

// テーブル表示
console.table(users);

// 実行時間の計測
console.time('database-query');
// ... クエリ処理 ...
console.timeEnd('database-query');

// スタックトレース
console.trace('Where is this called from?');

// グループ化された出力
console.group('User Details');
console.log('Name: Alice');
console.log('Role: admin');
console.groupEnd();
▶ 試してみよう

5. winstonによる構造化ロギング

(1) なぜconsole.logではなくwinstonを使うのか

console.log winston
ログレベルなし debug/info/warn/errorをサポート
ファイル出力なし Console + File + HTTPなどのtransportをサポート
フォーマットなし JSON / タイムスタンプ / カスタムフォーマットをサポート
ログローテーションなし winston-daily-rotate-fileで自動ローテーション
本番非推奨 本番レベルの選択肢

▶ サンプル:(2) インストールと基本セットアップ

BASH
npm install winston
npm install winston-daily-rotate-file
JAVASCRIPT
// logger.js
const winston = require('winston');
const DailyRotateFile = require('winston-daily-rotate-file');

const logger = winston.createLogger({
  level: 'info',
  format: winston.format.combine(
    winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
    winston.format.errors({ stack: true }),
    winston.format.json()
  ),
  defaultMeta: { service: 'my-app' },
  transports: [
    // エラーログ — 別ファイル
    new DailyRotateFile({
      filename: 'logs/error-%DATE%.log',
      datePattern: 'YYYY-MM-DD',
      level: 'error',
      maxSize: '20m',
      maxFiles: '14d',
    }),
    // 全ログ — 統合ファイル
    new DailyRotateFile({
      filename: 'logs/combined-%DATE%.log',
      datePattern: 'YYYY-MM-DD',
      maxSize: '20m',
      maxFiles: '14d',
    }),
  ],
});

// 開発時:コンソールにも色付きで出力
if (process.env.NODE_ENV !== 'production') {
  logger.add(new winston.transports.Console({
    format: winston.format.combine(
      winston.format.colorize(),
      winston.format.simple()
    ),
  }));
}

module.exports = logger;

▶ サンプル:(3) Expressアプリでの利用

JAVASCRIPT
// app-with-logger.js
const express = require('express');
const logger = require('./logger');

const app = express();
app.use(express.json());

// リクエストロギング用ミドルウェア
app.use((req, res, next) => {
  const start = Date.now();
  res.on('finish', () => {
    const duration = Date.now() - start;
    logger.info('HTTP request', {
      method: req.method,
      url: req.url,
      status: res.statusCode,
      duration: `${duration}ms`,
    });
  });
  next();
});

app.get('/api/health', (req, res) => {
  logger.debug('Health check requested');
  res.json({ status: 'ok', timestamp: new Date().toISOString() });
});

app.get('/api/users/:id', (req, res) => {
  const userId = req.params.id;
  logger.info('User lookup', { userId });

  if (isNaN(userId)) {
    logger.warn('Invalid user ID format', { userId });
    return res.status(400).json({ error: 'Invalid user ID' });
  }

  // ユーザー検索のシミュレーション
  const user = { id: parseInt(userId), name: 'Alice' };
  logger.info('User found', { userId: user.id, name: user.name });
  res.json(user);
});

app.use((err, req, res, next) => {
  logger.error('Unhandled error', { error: err.message, stack: err.stack });
  res.status(500).json({ error: 'Internal server error' });
});

module.exports = app;
▶ 試してみよう

ログファイルの出力例:

JSON
{"level":"info","message":"HTTP request","service":"my-app","timestamp":"2026-07-13 10:30:00","method":"GET","url":"/api/users/1","status":200,"duration":"15ms"}
{"level":"warn","message":"Invalid user ID format","service":"my-app","timestamp":"2026-07-13 10:30:05","userId":"abc"}
{"level":"error","message":"Unhandled error","service":"my-app","timestamp":"2026-07-13 10:31:00","error":"Cannot read property 'name' of undefined","stack":"TypeError: Cannot read property..."}

▶ サンプル:winstonによる構造化ロギング

JAVASCRIPT
const winston = require('winston');

const logger = winston.createLogger({
  level: 'info',
  format: winston.format.json(),
  transports: [new winston.transports.Console()],
});

logger.info('Server started', { port: 3000, env: 'development' });
logger.warn('Memory usage high', { used: '850MB' });
logger.error('Database connection failed', { db: 'main', retry: 3 });
▶ 試してみよう

winstonによる構造化JSONロギングを示します。異なるログレベル(infowarnerror)と、各ログエントリに付加されるコンテキストメタデータを使用します。




6. テストカバレッジとTDD

(1) テストカバレッジ

Jestにはカバレッジレポート用のIstanbulが同梱されています:

BASH
npm run test:coverage

Output:

TEXT 📖 参照専用
----------|---------|----------|---------|---------|-------------------
File      | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------|---------|----------|---------|---------|-------------------
All files |    90.5 |    85.71 |     100 |    90.5 |
 math.js  |     100 |      100 |     100 |     100 |
 app.js   |   88.24 |    83.33 |     100 |   88.24 | 45-48
----------|---------|----------|---------|---------|-------------------

カバレッジの各次元の説明:

次元 意味 推奨目標
ステートメント 実行されたステートメントの割合 ≥ 80%
ブランチ 実行された条件分岐の割合 ≥ 75%
関数 呼び出された関数の割合 ≥ 85%
実行されたコード行の割合 ≥ 80%

100%カバレッジを追い求めても費用対効果は薄れます。80%のカバレッジで大半のバグを検出できます。

▶ サンプル:(2) TDDワークフロー(Red-Green-Refactor)

100%
flowchart LR
    A["🔴 Red<br/>Write failing test"] --> B["🟢 Green<br/>Write minimal code to pass"]
    B --> C["🔵 Refactor<br/>Improve code while tests pass"]
    C --> A
  1. Red: 最初にテストを書く(機能がまだ存在しないため失敗する)
  2. Green: テストを通過させる最小限のコードを書く
  3. Refactor: テストが通ったままコードを改善する

▶ サンプル:TDDによる文字列ユーティリティ関数

JAVASCRIPT
// string-utils.test.js — 最初にテストを書く(Red)
const { capitalize, truncate, slugify } = require('./string-utils');

describe('StringUtils', () => {
  describe('capitalize', () => {
    test('should capitalize first letter', () => {
      expect(capitalize('hello')).toBe('Hello');
    });

    test('should handle empty string', () => {
      expect(capitalize('')).toBe('');
    });

    test('should handle already capitalized', () => {
      expect(capitalize('Hello')).toBe('Hello');
    });
  });

  describe('truncate', () => {
    test('should truncate long strings', () => {
      expect(truncate('Hello World', 5)).toBe('Hello...');
    });

    test('should not truncate short strings', () => {
      expect(truncate('Hi', 10)).toBe('Hi');
    });
  });

  describe('slugify', () => {
    test('should convert to URL slug', () => {
      expect(slugify('Hello World')).toBe('hello-world');
    });

    test('should handle special characters', () => {
      expect(slugify('C# & .NET!')).toBe('c-and-net');
    });
  });
});
▶ 試してみよう
JAVASCRIPT
// string-utils.js — テストの後に実装を書く(Green)
function capitalize(str) {
  if (!str) return '';
  return str.charAt(0).toUpperCase() + str.slice(1);
}

function truncate(str, maxLength) {
  if (str.length <= maxLength) return str;
  return str.slice(0, maxLength) + '...';
}

function slugify(str) {
  return str
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, '-')
    .replace(/^-|-$/g, '');
}

module.exports = { capitalize, truncate, slugify };


7. 総合例:タスク管理APIの完全なテストスイートを作成する

▶ サンプル:Tasks APIのテストスイート

ステップ1 — アプリケーションコード

JAVASCRIPT 📖 参照専用
// tasks-app.js
const express = require('express');
const app = express();
app.use(express.json());

let tasks = [];
let nextId = 1;

function resetTasks() {
  tasks = [];
  nextId = 1;
}

app.get('/api/tasks', (req, res) => {
  const { status, priority } = req.query;
  let filtered = tasks;
  if (status) filtered = filtered.filter(t => t.status === status);
  if (priority) filtered = filtered.filter(t => t.priority === priority);
  res.json(filtered);
});

app.get('/api/tasks/:id', (req, res) => {
  const task = tasks.find(t => t.id === parseInt(req.params.id));
  if (!task) return res.status(404).json({ error: 'Task not found' });
  res.json(task);
});

app.post('/api/tasks', (req, res) => {
  const { title, priority = 'medium' } = req.body;
  if (!title) return res.status(400).json({ error: 'Title is required' });
  const task = { id: nextId++, title, priority, status: 'pending' };
  tasks.push(task);
  res.status(201).json(task);
});

app.patch('/api/tasks/:id', (req, res) => {
  const task = tasks.find(t => t.id === parseInt(req.params.id));
  if (!task) return res.status(404).json({ error: 'Task not found' });
  const { title, status, priority } = req.body;
  if (title) task.title = title;
  if (status) task.status = status;
  if (priority) task.priority = priority;
  res.json(task);
});

app.delete('/api/tasks/:id', (req, res) => {
  const index = tasks.findIndex(t => t.id === parseInt(req.params.id));
  if (index === -1) return res.status(404).json({ error: 'Task not found' });
  const deleted = tasks.splice(index, 1);
  res.json(deleted[0]);
});

module.exports = { app, resetTasks };
論理コード 44 行(40 行制限超過、参照専用)

ステップ2 — 完全なテストスイート

JAVASCRIPT
// tasks-app.test.js
const request = require('supertest');
const { app, resetTasks } = require('./tasks-app');

beforeEach(() => {
  resetTasks();
});

describe('Tasks API — CRUD operations', () => {
  test('should create a task', async () => {
    const response = await request(app)
      .post('/api/tasks')
      .send({ title: 'Write tests', priority: 'high' });

    expect(response.status).toBe(201);
    expect(response.body).toMatchObject({
      id: 1,
      title: 'Write tests',
      priority: 'high',
      status: 'pending',
    });
  });

  test('should reject task without title', async () => {
    const response = await request(app)
      .post('/api/tasks')
      .send({ priority: 'high' });

    expect(response.status).toBe(400);
    expect(response.body.error).toBe('Title is required');
  });

  test('should list all tasks', async () => {
    await request(app).post('/api/tasks').send({ title: 'Task 1' });
    await request(app).post('/api/tasks').send({ title: 'Task 2' });

    const response = await request(app).get('/api/tasks');
    expect(response.status).toBe(200);
    expect(response.body).toHaveLength(2);
  });

  test('should filter tasks by status', async () => {
    await request(app).post('/api/tasks').send({ title: 'Pending task' });

    const createRes = await request(app).post('/api/tasks').send({ title: 'Done task' });
    await request(app).patch(`/api/tasks/${createRes.body.id}`).send({ status: 'done' });

    const response = await request(app).get('/api/tasks?status=done');
    expect(response.body).toHaveLength(1);
    expect(response.body[0].status).toBe('done');
  });

  test('should update a task', async () => {
    const createRes = await request(app).post('/api/tasks').send({ title: 'Old title' });

    const response = await request(app)
      .patch(`/api/tasks/${createRes.body.id}`)
      .send({ title: 'New title', status: 'done' });

    expect(response.status).toBe(200);
    expect(response.body.title).toBe('New title');
    expect(response.body.status).toBe('done');
  });

  test('should delete a task', async () => {
    const createRes = await request(app).post('/api/tasks').send({ title: 'To delete' });

    const response = await request(app).delete(`/api/tasks/${createRes.body.id}`);
    expect(response.status).toBe(200);

    const listRes = await request(app).get('/api/tasks');
    expect(listRes.body).toHaveLength(0);
  });

  test('should return 404 for non-existent task', async () => {
    const response = await request(app).get('/api/tasks/999');
    expect(response.status).toBe(404);
  });
});

実行:

BASH
npm test

Output:

TEXT 📖 参照専用
 PASS  ./tasks-app.test.js
  Tasks API — CRUD operations
    ✓ should create a task (15 ms)
    ✓ should reject task without title (3 ms)
    ✓ should list all tasks (4 ms)
    ✓ should filter tasks by status (8 ms)
    ✓ should update a task (5 ms)
    ✓ should delete a task (5 ms)
    ✓ should return 404 for non-existent task (2 ms)

Tests:       7 passed, 7 total
Time:        0.8s

❓ よくある質問

Q JestとMochaのどちらを選ぶべきですか?
A Jestはゼロ設定でモック/カバレッジが内蔵されており、ほとんどのプロジェクトに最適です。Mochaは柔軟ですがchai/sinon/istanbulが必要で、深いカスタマイズが必要なプロジェクト向けです。新規プロジェクトにはJestをおすすめします。
Q ユニットテストと統合テストの違いは何ですか?
A ユニットテストは個々の関数・モジュールを隔離してテストし、外部依存をモックで置き換えるため高速です。統合テストは複数のモジュール間(API + データベースなど)の連携を実コンポーネントで検証するため、遅いですが実運用に近いです。
Q テストに実際のデータベース接続は必要ですか?
A ユニットテストでは実際のデータベースを決して使わず、モックを使用します。統合テストではインメモリデータベース(SQLiteの:memory:など)や専用のテスト用データベースを使用できます。E2Eテストではテスト環境のデータベースを使用します。
Q winstonのログレベルは何ですか?
A 低い方から順に:error → warn → info → http → verbose → debug → silly。レベルをinfoに設定すると、info以上のみが出力されます。
Q Jestのテスト自体をデバッグするにはどうすればよいですか?
A node --inspect-brk node_modules/.bin/jest --runInBandを実行し、Chrome DevToolsでデバッグします。--runInBandはJestを単一スレッドで実行し、ブレークポイントを機能させます。
Q Supertestは実際のサーバーを起動する必要がありますか?
A いいえ。Supertestはhttp.createServer(app)でインメモリサーバーを作成します。ポートは占有されず、テスト終了時に自動的に閉じられます。

📖 まとめ


📝 練習問題

  1. 次の関数に対するJestのユニットテストを書きましょう:function isPalindrome(str) { return str === str.split('').reverse().join(''); }。通常の回文、非回文、空文字列、大文字小文字混在のケースをカバーしてください。
  2. Express API(/api/booksのCRUD)を作成し、Supertestで少なくとも5件の統合テスト(作成、取得、更新、削除、404)を書きましょう。
  3. 既存のExpressアプリにwinstonのロギングミドルウェアを追加し、全リクエストのメソッド/URL/ステータス/所要時間を記録し、日次ログローテーションを設定しましょう。
  4. TDDでformatCurrency(amount, currency)関数を開発しましょう。まず失敗するテストを書き(例:formatCurrency(1234.5, 'USD')'$1,234.50')、その後実装を書きます。
  5. jest --coverageを実行し、カバレッジが80%未満のファイルを特定し、目標達成のためのテストケースを追加しましょう。

Web-Tutorial.com

Web-Tutorial 技術チーム

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

100%