NodeJS 速習チュートリアル

Node.js 組み込みテストランナー(node:test)

1. Node.js テストランナー入門

組み込みの node:test モジュールは、外部ライブラリに依存せず、Node.js 上で直接 JavaScript のテストを記述・実行するための軽量なフレームワークを提供します。

Node.js 20 で安定版(Stable API)として導入されたこの機能は、外部のテスティングフレームワークに代わる、高速でモダンな選択肢として設計されています。

       注意: Node.js テストランナーは Node.js v20 以降で安定版となっています。古いバージョンでは一部のアドバンスドな機能が実験的(Experimental)である場合があります。

1.1 主な特徴

コア機能

  • ゼロ構成(Zero Configuration): セットアップなしですぐに動作します。
  • デュアルモジュールサポート: ネイティブ ESM および CommonJS との互換性があります。
  • 並列実行: デフォルトでテストが並列(コンカレント)に実行されます。
  • テストの分離(Isolation): 各テストファイルは独自のコンテキストで実行されます。

アドバンスド機能

  • 非同期(Async)サポート: async/await ハンドリングを標準サポートしています。
  • テストフック: セットアップやクリーンアップのための Before/After フックが利用可能です。
  • モッキング(Mocking): テストダブルやスパイ機能が組み込まれています。
  • コードカバレッジ: Node.js のカバレッジツールと統合されています。

2. はじめに

2.1 最初のテストを作成する

Node.js テストランナーを使用して、基本的なテストを作成・実行してみましょう。Node.js 16.17.0 以降がインストールされている必要があります。

1. テストファイルの作成 (test/example.test.js)

// テストモジュールをロード
const test = require('node:test');
// より良いエラーメッセージのために厳密なアサーションモードを使用
const assert = require('node:assert/strict');

// シンプルな同期テスト
test('基本的な算術演算', (t) => {
  // 1 + 1 が 2 になることをアサート
  assert.equal(1 + 1, 2, '1 + 1 は 2 になるべきです');

  // オブジェクトや配列のディープ等価性チェック
  assert.deepEqual(
    { a: 1, b: { c: 2 } },
    { a: 1, b: { c: 2 } }
  );
});

// async/await を使用した非同期テスト
test('非同期テスト', async (t) => {
  const result = await Promise.resolve('非同期の結果');
  assert.strictEqual(result, '非同期の結果');
});

2. テストの実行

# test ディレクトリ内のすべてのテストファイルを実行
node --test

# 特定のテストファイルを実行
node --test test/example.test.js

# カバレッジレポート付きで実行
NODE_V8_COVERAGE=coverage node --test

2.2 テストの構造と整理

大規模なプロジェクトでは、テストを構造化して整理します。

project/
├── src/
│   ├── math.js
│   └── utils.js
└── test/
    ├── unit/
    │   ├── math.test.js
    │   └── utils.test.js
    └── integration/
        └── api.test.js

2.3 テストフック

テスト環境のセットアップとクリーンアップにはフックを使用します。

const { test, describe, before, after, beforeEach, afterEach } = require('node:test');
const assert = require('node:assert/strict');

describe('フックを使用したテストスイート', (t) => {
  let testData = [];

  // すべてのテストの前に一度だけ実行
  before(() => {
    console.log('全テストの実行前に開始');
    testData = [1, 2, 3];
  });

  // 各テストの前に実行
  beforeEach((t) => {
    console.log('各テストの実行前に開始');
  });

  test('配列の長さ', () => {
    assert.strictEqual(testData.length, 3);
  });

  // 各テストの後に実行
  afterEach(() => {
    console.log('各テストの実行後に完了');
  });

  // すべてのテストの後に一度だけ実行
  after(() => {
    console.log('全テストの完了後にクリーンアップ');
    testData = [];
  });
});

2.3 CommonJS の構文

// simple-test.js
const test = require('node:test');
const assert = require('node:assert/strict');

test('基本的なテスト', () => {
  assert.equal(1 + 1, 2);
});

3. テストの実行(詳細)

--test フラグを使用してテストを実行します。

node --test simple-test.js

ディレクトリ内のすべてのテストファイルを一括実行することも可能です。

node --test

このコマンドは、以下のパターンに一致するファイルをすべて実行します:

  • **/*.test.js
  • **/*.spec.js
  • **/test-*.js
  • **/test/*.js

4. テストの記述

4.1 非同期テスト

非同期コードの場合は、async テスト関数を使用します。

import test from 'node:test';
import assert from 'node:assert/strict';

// async/await を使用する場合
test('非同期テスト', async () => {
  // 非同期操作のシミュレーション
  const result = await Promise.resolve(42);
  assert.equal(result, 42);
});

// コールバックと done を使用する場合(古いスタイル)
test('コールバックテスト', (t, done) => {
  setTimeout(() => {
    assert.equal(1 + 1, 2);
    done();
  }, 100);
});

4.2 サブテスト(ネストされたテスト)

サブテストを使用して関連するテストを整理できます。

import test from 'node:test';
import assert from 'node:assert/strict';

test('数学的演算', async (t) => {
  await t.test('加算', () => {
    assert.equal(1 + 1, 2);
  });
  
  await t.test('乗算', () => {
    assert.equal(2 * 3, 6);
  });
  
  await t.test('除算', () => {
    assert.equal(10 / 2, 5);
  });
});

4.3 セットアップとティアダウン(テストフィクスチャ)

個別のテストでセットアップやティアダウンが必要な場合は、t.before() および t.after() フックを使用します。

import test from 'node:test';
import assert from 'node:assert/strict';

test('テストフィクスチャの使用', async (t) => {
  // セットアップ - テストの前に実行
  t.before(() => {
    console.log('テストリソースをセットアップ中');
    // 例: テスト用データベースの作成、サービスのモッキングなど
  });
  
  // 実際のテスト
  await t.test('フィクスチャを使用したテスト本体', () => {
    assert.equal(1 + 1, 2);
  });
  
  // ティアダウン - テストの後に実行
  t.after(() => {
    console.log('テストリソースをクリーンアップ中');
    // 例: テスト用データベースの削除、モックの復元など
  });
});

4.4 テストのスキップと TODO

テストをスキップしたり、TODO としてマークしたりできます。

import test from 'node:test';

// このテストをスキップ
test('スキップされたテスト', { skip: true }, () => {
  // 実行されません
});

// 理由付きでスキップ
test('理由付きでスキップ', { skip: '後で対応予定' }, () => {
  // 実行されません
});

// TODO としてマーク
test('TODO テスト', { todo: true }, () => {
  // 実行されませんが、レポートには TODO として表示されます
});

// 条件付きスキップ
test('条件付きスキップ', { skip: process.platform === 'win32' }, () => {
  // Windows 上でのみスキップされます
});

5. アサーション(Assertions)

Node.js テストランナーは、標準の assert モジュールと連携します。より厳密な比較には assert/strict を使用してください。

5.1 一般的なアサーション

import assert from 'node:assert/strict';

// 等価性のチェック
assert.equal(1, 1);               // 緩やかな比較 (==)
assert.strictEqual(1, 1);         // 厳密な比較 (===)
assert.deepEqual({a: 1}, {a: 1});   // オブジェクトのディープな比較
assert.deepStrictEqual({a: 1}, {a: 1}); // オブジェクトの厳密なディープ比較

// 真偽値のチェック
assert.ok(true);                    // 値が真(truthy)かチェック
assert.ok(1);                       // これも真

// 値の比較
assert.notEqual(1, 2);              // 不一致をチェック
assert.notStrictEqual(1, '1');      // 厳密な不一致をチェック

// エラーのスロー
assert.throws(() => { throw new Error('ドカン!'); }); // 関数がエラーを投げるか
assert.doesNotThrow(() => { return 42; });           // エラーが投げられないか

// 非同期アサーション
await assert.rejects(               // Promise がリジェクトされるか
  async () => { throw new Error('非同期ドカン!'); }
);

6. モッキングの活用

Node.js テストランナーには高度なモッキング機能は内蔵されていませんが、以下の方法で対応可能です:

  • 依存性の注入(Dependency Injection)を使用してテストダブルを提供
  • シンプルなモック関数やオブジェクトを作成
  • 必要に応じてサードパーティのモッキングライブラリ(Sinon 等)と統合

6.1 シンプルなモックの例

import test from 'node:test';
import assert from 'node:assert/strict';

// テスト対象の関数
function processUser(user, logger) {
  if (!user.name) {
    logger.error('ユーザー名がありません');
    return false;
  }
  logger.info(`ユーザーを処理中: ${user.name}`);
  return true;
}

// モックロガーを使用したテスト
test('processUser が正しくログを出力すること', () => {
  // モックロガーの作成
  const mockCalls = [];
  const mockLogger = {
    error: (msg) => mockCalls.push(['error', msg]),
    info: (msg) => mockCalls.push(['info', msg])
  };
  
  // 有効なユーザーでのテスト
  const validResult = processUser({name: 'Alice'}, mockLogger);
  assert.strictEqual(validResult, true);
  assert.deepStrictEqual(mockCalls[0], ['info', 'ユーザーを処理中: Alice']);
  
  // モックのコール履歴をリセット
  mockCalls.length = 0;
  
  // 無効なユーザーでのテスト
  const invalidResult = processUser({}, mockLogger);
  assert.strictEqual(invalidResult, false);
  assert.deepStrictEqual(mockCalls[0], ['error', 'ユーザー名がありません']);
});

7. 実践的なテスト例

7.1 ユーティリティ関数のテスト

// utils.js
exports.formatPrice = function(price) {
  if (typeof price !== 'number' || isNaN(price)) {
    throw new Error('価格は有効な数値である必要があります');
  }
  return `$${price.toFixed(2)}`;
};

// utils.test.js
const test = require('node:test');
const assert = require('node:assert/strict');
const { formatPrice } = require('./utils');

// テストケース
test('formatPrice は数値を通貨文字列にフォーマットする', (t) => {
  assert.equal(formatPrice(10), '$10.00');
  assert.equal(formatPrice(10.5), '$10.50');
  assert.equal(formatPrice(0), '$0.00');
});

// エラーのテスト
test('formatPrice は無効な入力に対してエラーを投げる', (t) => {
  assert.throws(() => formatPrice('数値ではない'), {
    message: '価格は有効な数値である必要があります'
  });
  assert.throws(() => formatPrice(NaN));
  assert.throws(() => formatPrice());
});

API エンドポイントのテスト

// userService.js
const express = require('express');
const app = express();
app.use(express.json());

app.get('/users/:id', (req, res) => {
  const userId = parseInt(req.params.id);
  // 簡易化:実際のアプリではデータベースから取得
  if (userId === 1) {
    res.json({ id: 1, name: 'John Doe', email: '[email protected]' });
  } else {
    res.status(404).json({ error: 'ユーザーが見つかりません' });
  }
});

module.exports = app;

// userService.test.js
const test = require('node:test');
const assert = require('node:assert/strict');
const http = require('node:http');
const app = require('./userService');

test('GET /users/:id が正しいユーザーを返す', async (t) => {
  // サーバーの起動
  const server = http.createServer(app);
  await new Promise(resolve => server.listen(0, resolve));
  const port = server.address().port;
  
  try {
    // API へのリクエスト実行
    const response = await fetch(`http://localhost:${port}/users/1`);
    assert.equal(response.status, 200, 'ステータスコードは 200 であるべきです');
    
    const user = await response.json();
    assert.deepStrictEqual(user, {
      id: 1,
      name: 'John Doe',
      email: '[email protected]'
    });
    
    // 見つからない場合のテスト
    const notFoundResponse = await fetch(`http://localhost:${port}/users/999`);
    assert.equal(notFoundResponse.status, 404, 'ステータスコードは 404 であるべきです');
  } finally {
    // クリーンアップ - サーバーを閉じる
    await new Promise(resolve => server.close(resolve));
  }
});

8. 高度な設定

8.1 カスタムレポーター

テスト結果の出力形式を指定できます:

node --test --test-reporter=spec

利用可能なレポーター:

  • spec - 詳細な階層表示
  • dot - 最小限のドット表示
  • tap - Test Anything Protocol 形式
  • junit - JUnit XML 形式

8.2 テストのフィルタリング

パターンを使用して実行するテストをフィルタリングできます:

node --test --test-name-pattern="user"

名前の中に "user" を含むテストのみが実行されます。

8.3 ウォッチモード

開発中にファイルが変更されたら自動的に再実行します:

node --test --watch

9. 他のテスティングフレームワークとの比較

機能Node.js テストランナーJestMochaVitest
標準搭載✅ あり (Node 16.17.0+)❌ なし❌ なし❌ なし
ゼロ構成✅ 対応✅ 対応❌ セットアップが必要✅ 対応
ランナーNode.js 組み込みJestMochaVite
アサーションnode:assertJest ExpectChai/SinonJest互換
並列実行✅ 対応✅ 対応✅ --parallel で対応✅ 対応
カバレッジ✅ NODE_V8_COVERAGE✅ 内蔵❌ nyc等が必要✅ 内蔵
ウォッチモード✅ あり (--watch)✅ あり✅ --watch で対応✅ 高速な HMR
最適な用途標準・軽量・依存なし多機能なテスト柔軟なテスト構成Vite・ESM環境

       まとめ: Node.js テストランナーは、Node.js 自体に組み込まれた「軽量で依存関係のないテストソリューション」を求めるプロジェクトに最適です。より複雑な要件がある場合は、Jest や Mocha が依然として有力な選択肢となります。