はじめに#
Node.jsでユニットテストを書くとき、多くの開発者はJestやMochaといった外部ライブラリを選択してきました。しかし、Node.js 18で実験的に導入され、Node.js 20で安定版(Stable)となった組み込みテストランナー node:test を使えば、追加の依存関係なしにテストを書くことができます。
本記事では、node:testモジュールの基本的な使い方から、describe/it/testによる構文、node:assert/strictを使ったアサーション、フック関数、モック機能、そしてカバレッジ計測まで、組み込みテストランナーを活用するために必要な知識を網羅的に解説します。
node:testとは#
node:testは、Node.jsに組み込まれたテストランナーモジュールです。従来はJestやMochaなどの外部ライブラリが必要でしたが、このモジュールを使えばnpmパッケージの追加インストールなしにテストを実行できます。
node:testの主な特徴#
| 特徴 |
説明 |
| ゼロ依存 |
Node.jsに組み込み済み、追加パッケージ不要 |
| 標準的なAPI |
describe/it/testなど馴染みのある構文 |
| 組み込みアサーション |
node:assertモジュールとの統合 |
| モック機能 |
関数やタイマーのモックを標準搭載 |
| カバレッジ計測 |
V8のカバレッジ機能を活用 |
| Watchモード |
ファイル変更時の自動再実行 |
なぜnode:testを選ぶのか#
- 依存関係の削減:
package.jsonのdevDependenciesを最小限に保てる
- メンテナンスコストの低減: Node.jsのバージョンアップに追従するだけで最新機能を利用可能
- 起動の高速化: 外部パッケージの読み込みが不要なため、テスト開始が速い
- 学習コストの低減: 他のテストフレームワークと似た構文で習得しやすい
前提条件と実行環境#
本記事の内容を実践するには、以下の環境が必要です。
| 項目 |
要件 |
| Node.js |
v20.x LTS以上(v22.x推奨) |
| npm |
v10.x以上 |
| OS |
Windows / macOS / Linux |
| 前提知識 |
JavaScriptの基礎、Node.jsの基本API |
Node.jsのバージョンを確認するには、ターミナルで以下を実行します。
1
2
3
4
5
|
node -v
# v22.12.0
npm -v
# 10.9.0
|
プロジェクトのセットアップ#
まず、テスト用のプロジェクトを作成します。
1
2
3
|
mkdir node-test-tutorial
cd node-test-tutorial
npm init -y
|
package.jsonにテスト用のnpmスクリプトを追加します。
1
2
3
4
5
6
7
8
9
10
|
{
"name": "node-test-tutorial",
"version": "1.0.0",
"type": "module",
"scripts": {
"test": "node --test",
"test:watch": "node --test --watch",
"test:coverage": "node --test --experimental-test-coverage"
}
}
|
これで準備は完了です。外部パッケージのインストールは一切不要です。
基本的なテストの書き方#
test()関数によるシンプルなテスト#
最もシンプルなテストは、test()関数を使って記述します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
// math.test.js
import { test } from 'node:test';
import assert from 'node:assert/strict';
// テスト対象の関数
function add(a, b) {
return a + b;
}
// 同期テスト
test('add関数は2つの数値を足し合わせる', () => {
const result = add(2, 3);
assert.strictEqual(result, 5);
});
// 非同期テスト
test('非同期処理のテスト', async () => {
const result = await Promise.resolve(42);
assert.strictEqual(result, 42);
});
|
テストを実行するには、以下のコマンドを使用します。
1
|
node --test math.test.js
|
実行結果は以下のようになります。
1
2
3
4
5
6
7
8
9
10
|
✔ add関数は2つの数値を足し合わせる (0.5ms)
✔ 非同期処理のテスト (0.1ms)
ℹ tests 2
ℹ suites 0
ℹ pass 2
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 50
|
describe()とit()によるテストの構造化#
JestやMochaと同様に、describe()とit()を使ってテストをグループ化できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
|
// calculator.test.js
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
// テスト対象
class Calculator {
add(a, b) {
return a + b;
}
subtract(a, b) {
return a - b;
}
multiply(a, b) {
return a * b;
}
divide(a, b) {
if (b === 0) {
throw new Error('Division by zero');
}
return a / b;
}
}
describe('Calculator', () => {
const calc = new Calculator();
describe('add', () => {
it('正の数同士を足し合わせる', () => {
assert.strictEqual(calc.add(2, 3), 5);
});
it('負の数を含む計算ができる', () => {
assert.strictEqual(calc.add(-1, 5), 4);
});
});
describe('subtract', () => {
it('引き算ができる', () => {
assert.strictEqual(calc.subtract(10, 4), 6);
});
});
describe('divide', () => {
it('正常に除算できる', () => {
assert.strictEqual(calc.divide(10, 2), 5);
});
it('ゼロ除算でエラーをスローする', () => {
assert.throws(
() => calc.divide(10, 0),
{ message: 'Division by zero' }
);
});
});
});
|
テストファイルの命名規則と自動検出#
node --testコマンドを引数なしで実行すると、以下のパターンに一致するファイルが自動的にテストとして実行されます。
**/*.test.{js,mjs,cjs}
**/*-test.{js,mjs,cjs}
**/*_test.{js,mjs,cjs}
**/test-*.{js,mjs,cjs}
**/test.{js,mjs,cjs}
**/test/**/*.{js,mjs,cjs}
TypeScriptファイル(.ts、.mts、.cts)も、--no-strip-typesオプションなしで同様に検出されます。
1
2
3
4
5
|
# カレントディレクトリ以下のすべてのテストファイルを実行
node --test
# 特定のディレクトリのみを対象にする
node --test "./src/**/*.test.js"
|
node:assert/strictによるアサーション#
Node.jsにはnode:assertモジュールが組み込まれており、テストでの検証に使用できます。Strict Assertion Modeを使用することで、より厳密な比較が行われます。
基本的なアサーション#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
import { test } from 'node:test';
import assert from 'node:assert/strict';
test('strictEqual - 厳密等価', () => {
assert.strictEqual(1, 1); // OK
// assert.strictEqual(1, '1'); // NG: 型が異なる
});
test('deepStrictEqual - オブジェクトの深い比較', () => {
const obj1 = { a: 1, b: { c: 2 } };
const obj2 = { a: 1, b: { c: 2 } };
assert.deepStrictEqual(obj1, obj2); // OK: 構造と値が一致
});
test('notStrictEqual - 不等価の検証', () => {
assert.notStrictEqual(1, 2); // OK
});
test('ok - truthy値の検証', () => {
assert.ok(true); // OK
assert.ok(1); // OK
assert.ok('string'); // OK
});
|
エラーのアサーション#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
import { test } from 'node:test';
import assert from 'node:assert/strict';
function divide(a, b) {
if (b === 0) throw new Error('Cannot divide by zero');
return a / b;
}
test('throws - 例外がスローされることを検証', () => {
// エラーがスローされることを確認
assert.throws(
() => divide(10, 0),
Error
);
// エラーメッセージも検証
assert.throws(
() => divide(10, 0),
{ message: 'Cannot divide by zero' }
);
// 正規表現でメッセージを検証
assert.throws(
() => divide(10, 0),
/divide by zero/
);
});
test('doesNotThrow - 例外がスローされないことを検証', () => {
assert.doesNotThrow(() => divide(10, 2));
});
|
非同期処理のアサーション#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
import { test } from 'node:test';
import assert from 'node:assert/strict';
async function fetchData(shouldFail) {
if (shouldFail) {
throw new Error('Fetch failed');
}
return { data: 'success' };
}
test('rejects - Promiseがrejectされることを検証', async () => {
await assert.rejects(
async () => fetchData(true),
{ message: 'Fetch failed' }
);
});
test('doesNotReject - Promiseがrejectされないことを検証', async () => {
await assert.doesNotReject(async () => fetchData(false));
});
|
主要なアサーションメソッド一覧#
| メソッド |
説明 |
strictEqual(actual, expected) |
厳密等価(===)で比較 |
notStrictEqual(actual, expected) |
厳密不等価(!==)で比較 |
deepStrictEqual(actual, expected) |
オブジェクトの深い比較 |
notDeepStrictEqual(actual, expected) |
オブジェクトが異なることを検証 |
ok(value) |
値がtruthyであることを検証 |
throws(fn, error) |
関数が例外をスローすることを検証 |
doesNotThrow(fn) |
関数が例外をスローしないことを検証 |
rejects(asyncFn, error) |
Promiseがrejectされることを検証 |
doesNotReject(asyncFn) |
Promiseがrejectされないことを検証 |
match(string, regexp) |
文字列が正規表現にマッチすることを検証 |
doesNotMatch(string, regexp) |
文字列が正規表現にマッチしないことを検証 |
フック関数(before/after/beforeEach/afterEach)#
テストのセットアップやクリーンアップには、フック関数を使用します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
import { describe, it, before, after, beforeEach, afterEach } from 'node:test';
import assert from 'node:assert/strict';
describe('フック関数のデモ', () => {
let database;
let testData;
// スイート全体の前に1回だけ実行
before(() => {
console.log('before: データベース接続を初期化');
database = { connected: true, data: [] };
});
// スイート全体の後に1回だけ実行
after(() => {
console.log('after: データベース接続をクローズ');
database = null;
});
// 各テストの前に実行
beforeEach(() => {
console.log('beforeEach: テストデータをリセット');
testData = { id: 1, name: 'Test User' };
database.data = [testData];
});
// 各テストの後に実行
afterEach(() => {
console.log('afterEach: クリーンアップ');
database.data = [];
});
it('データを取得できる', () => {
assert.strictEqual(database.data.length, 1);
assert.deepStrictEqual(database.data[0], testData);
});
it('データを追加できる', () => {
database.data.push({ id: 2, name: 'Another User' });
assert.strictEqual(database.data.length, 2);
});
it('前のテストの影響を受けない', () => {
// beforeEachでリセットされるため、常に1件
assert.strictEqual(database.data.length, 1);
});
});
|
TestContextを使ったフック#
テストコンテキスト(tパラメータ)を使って、テストごとにフックを設定することもできます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
import { test } from 'node:test';
import assert from 'node:assert/strict';
test('コンテキストベースのフック', async (t) => {
let resource;
// このテストとサブテストの前に実行
t.before(() => {
resource = { initialized: true };
});
// このテストとサブテストの後に実行
t.after(() => {
resource = null;
});
await t.test('サブテスト1', () => {
assert.ok(resource.initialized);
});
await t.test('サブテスト2', () => {
assert.ok(resource.initialized);
});
});
|
モック機能#
node:testモジュールには、関数やタイマーをモックする機能が組み込まれています。
関数のモック#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
import { test, mock } from 'node:test';
import assert from 'node:assert/strict';
test('関数のモック', () => {
// モック関数の作成
const mockFn = mock.fn((a, b) => a + b);
// モック関数を呼び出し
const result1 = mockFn(2, 3);
const result2 = mockFn(10, 20);
// 呼び出し回数の検証
assert.strictEqual(mockFn.mock.callCount(), 2);
// 呼び出し引数の検証
assert.deepStrictEqual(mockFn.mock.calls[0].arguments, [2, 3]);
assert.deepStrictEqual(mockFn.mock.calls[1].arguments, [10, 20]);
// 戻り値の検証
assert.strictEqual(mockFn.mock.calls[0].result, 5);
assert.strictEqual(mockFn.mock.calls[1].result, 30);
});
|
メソッドのモック#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
import { test, mock } from 'node:test';
import assert from 'node:assert/strict';
test('オブジェクトメソッドのモック', (t) => {
const calculator = {
add(a, b) {
return a + b;
}
};
// メソッドをモック(テストコンテキスト経由で自動リストア)
t.mock.method(calculator, 'add', () => 999);
assert.strictEqual(calculator.add(2, 3), 999);
assert.strictEqual(calculator.add.mock.callCount(), 1);
});
test('元のメソッドは復元されている', () => {
const calculator = {
add(a, b) {
return a + b;
}
};
// 前のテストのモックは影響しない
assert.strictEqual(calculator.add(2, 3), 5);
});
|
タイマーのモック#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
import { test, mock } from 'node:test';
import assert from 'node:assert/strict';
test('setTimeoutのモック', (t) => {
// タイマーをモック
t.mock.timers.enable({ apis: ['setTimeout'] });
const callback = t.mock.fn();
setTimeout(callback, 5000);
// まだ呼ばれていない
assert.strictEqual(callback.mock.callCount(), 0);
// 時間を進める
t.mock.timers.tick(5000);
// コールバックが呼ばれた
assert.strictEqual(callback.mock.callCount(), 1);
});
test('Dateのモック', (t) => {
t.mock.timers.enable({ apis: ['Date'] });
// 特定の日時に設定
t.mock.timers.setTime(new Date('2026-01-07T12:00:00Z').getTime());
const now = new Date();
assert.strictEqual(now.toISOString(), '2026-01-07T12:00:00.000Z');
});
|
テストのスキップとTODO#
開発中のテストや一時的に無効化したいテストには、skipやtodoを使用します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
import { test, describe, it } from 'node:test';
import assert from 'node:assert/strict';
// テストをスキップ
test('スキップされるテスト', { skip: true }, () => {
assert.fail('このテストは実行されない');
});
// 理由付きでスキップ
test('メンテナンス中', { skip: '外部APIの更新待ち' }, () => {
assert.fail('このテストは実行されない');
});
// TODOとしてマーク(実行はされるが失敗してもテスト全体は失敗しない)
test('未実装の機能', { todo: true }, () => {
throw new Error('まだ実装されていない');
});
// ショートハンド記法
test.skip('これもスキップ', () => {});
test.todo('これもTODO', () => {});
describe('機能グループ', () => {
it.skip('スキップされるit', () => {});
it.todo('TODOのit', () => {});
});
|
onlyによる特定テストの実行#
デバッグ時に特定のテストだけを実行したい場合は、onlyオプションを使用します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
import { test } from 'node:test';
import assert from 'node:assert/strict';
// --test-onlyフラグと組み合わせて使用
test('通常のテスト', () => {
assert.ok(true);
});
test.only('このテストだけ実行', () => {
assert.ok(true);
});
test('これは実行されない', () => {
assert.ok(true);
});
|
onlyを有効にするには、--test-onlyフラグを付けてテストを実行します。
1
|
node --test --test-only
|
テストカバレッジの計測#
--experimental-test-coverageフラグを使用すると、コードカバレッジを計測できます。
1
|
node --test --experimental-test-coverage
|
カバレッジ計測の実践例#
テスト対象のファイルを作成します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
// src/utils.js
export function isEven(n) {
return n % 2 === 0;
}
export function isPositive(n) {
if (n > 0) {
return true;
}
return false;
}
export function greet(name) {
if (!name) {
return 'Hello, Guest!';
}
return `Hello, ${name}!`;
}
|
テストファイルを作成します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
// src/utils.test.js
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { isEven, isPositive, greet } from './utils.js';
describe('utils', () => {
describe('isEven', () => {
it('偶数の場合trueを返す', () => {
assert.strictEqual(isEven(2), true);
assert.strictEqual(isEven(0), true);
});
it('奇数の場合falseを返す', () => {
assert.strictEqual(isEven(1), false);
assert.strictEqual(isEven(3), false);
});
});
describe('isPositive', () => {
it('正の数の場合trueを返す', () => {
assert.strictEqual(isPositive(1), true);
});
// 負の数のテストを追加してカバレッジを上げる
it('負の数の場合falseを返す', () => {
assert.strictEqual(isPositive(-1), false);
});
});
describe('greet', () => {
it('名前がある場合は名前付きの挨拶を返す', () => {
assert.strictEqual(greet('Alice'), 'Hello, Alice!');
});
it('名前がない場合はゲスト用の挨拶を返す', () => {
assert.strictEqual(greet(''), 'Hello, Guest!');
assert.strictEqual(greet(null), 'Hello, Guest!');
});
});
});
|
カバレッジを計測して実行します。
1
|
node --test --experimental-test-coverage src/
|
出力例は以下のようになります。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
✔ utils > isEven > 偶数の場合trueを返す (0.5ms)
✔ utils > isEven > 奇数の場合falseを返す (0.1ms)
✔ utils > isPositive > 正の数の場合trueを返す (0.1ms)
✔ utils > isPositive > 負の数の場合falseを返す (0.1ms)
✔ utils > greet > 名前がある場合は名前付きの挨拶を返す (0.1ms)
✔ utils > greet > 名前がない場合はゲスト用の挨拶を返す (0.1ms)
ℹ tests 6
ℹ suites 4
ℹ pass 6
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 60
ℹ start of coverage report
ℹ -------------------------------------------
ℹ file | line % | branch % | funcs %
ℹ -------------------------------------------
ℹ src/utils.js | 100.00 | 100.00 | 100.00
ℹ -------------------------------------------
ℹ all files | 100.00 | 100.00 | 100.00
ℹ -------------------------------------------
ℹ end of coverage report
|
カバレッジの除外設定#
特定のコード行をカバレッジ計測から除外するには、コメントを使用します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
/* node:coverage disable */
if (process.env.DEBUG) {
// デバッグ専用のコード
console.log('Debug mode enabled');
}
/* node:coverage enable */
// 1行だけ除外
/* node:coverage ignore next */
const unusedVariable = 'this line is ignored';
// 複数行を除外
/* node:coverage ignore next 3 */
if (someRareCondition) {
handleRareCase();
}
|
Watchモード#
開発中はWatchモードを使用すると、ファイルの変更を検知して自動的にテストが再実行されます。
package.jsonでのスクリプト設定#
1
2
3
4
5
6
7
8
|
{
"scripts": {
"test": "node --test",
"test:watch": "node --test --watch",
"test:coverage": "node --test --experimental-test-coverage",
"test:verbose": "node --test --test-reporter spec"
}
}
|
テストレポーター#
node:testは複数の出力形式をサポートしています。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
# spec形式(デフォルト)
node --test --test-reporter spec
# TAP形式
node --test --test-reporter tap
# dot形式(コンパクト)
node --test --test-reporter dot
# JUnit XML形式
node --test --test-reporter junit
# 複数のレポーターを同時に使用
node --test --test-reporter spec --test-reporter-destination stdout \
--test-reporter junit --test-reporter-destination results.xml
|
実践的なテスト構成例#
実際のプロジェクトでの構成例を示します。
ディレクトリ構造#
1
2
3
4
5
6
7
8
9
10
11
12
13
|
project/
├── package.json
├── src/
│ ├── services/
│ │ ├── userService.js
│ │ └── userService.test.js
│ ├── utils/
│ │ ├── validator.js
│ │ └── validator.test.js
│ └── index.js
└── test/
└── integration/
└── api.test.js
|
テスト対象のコード#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
// src/services/userService.js
export class UserService {
constructor(repository) {
this.repository = repository;
}
async findById(id) {
if (!id) {
throw new Error('ID is required');
}
return this.repository.findById(id);
}
async create(userData) {
if (!userData.email) {
throw new Error('Email is required');
}
return this.repository.create(userData);
}
}
|
テストコード#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
|
// src/services/userService.test.js
import { describe, it, beforeEach } from 'node:test';
import assert from 'node:assert/strict';
import { UserService } from './userService.js';
describe('UserService', () => {
let userService;
let mockRepository;
beforeEach((t) => {
// リポジトリのモック
mockRepository = {
findById: t.mock.fn(async (id) => ({ id, name: 'Test User' })),
create: t.mock.fn(async (data) => ({ id: 1, ...data }))
};
userService = new UserService(mockRepository);
});
describe('findById', () => {
it('IDでユーザーを取得できる', async () => {
const user = await userService.findById(1);
assert.deepStrictEqual(user, { id: 1, name: 'Test User' });
assert.strictEqual(mockRepository.findById.mock.callCount(), 1);
});
it('IDがない場合はエラーをスローする', async () => {
await assert.rejects(
async () => userService.findById(null),
{ message: 'ID is required' }
);
});
});
describe('create', () => {
it('新規ユーザーを作成できる', async () => {
const userData = { email: 'test@example.com', name: 'New User' };
const user = await userService.create(userData);
assert.strictEqual(user.id, 1);
assert.strictEqual(user.email, 'test@example.com');
});
it('メールがない場合はエラーをスローする', async () => {
await assert.rejects(
async () => userService.create({ name: 'No Email' }),
{ message: 'Email is required' }
);
});
});
});
|
node:testとJestの比較#
| 機能 |
node:test |
Jest |
| インストール |
不要 |
必要 |
| describe/it/test |
対応 |
対応 |
| アサーション |
node:assert使用 |
expect()使用 |
| モック |
mock.fn()使用 |
jest.fn()使用 |
| スナップショット |
対応(v22.3.0以降) |
対応 |
| カバレッジ |
–experimental-test-coverage |
–coverage |
| Watchモード |
–watch |
–watch |
| 並列実行 |
対応 |
対応 |
| TypeScript |
追加設定必要 |
ts-jest必要 |
まとめ#
本記事では、Node.jsの組み込みテストランナーnode:testについて解説しました。
学んだ内容を振り返ります。
node:testモジュールは外部依存なしでテストを実行できる
describe/it/testによる構造化されたテストの記述方法
node:assert/strictによる厳密なアサーション
before/after/beforeEach/afterEachによるテストのセットアップとクリーンアップ
- モック機能を使った関数やタイマーのテスト
--experimental-test-coverageによるカバレッジ計測
Node.js組み込みテストランナーは、シンプルなプロジェクトや依存関係を最小限にしたい場合に特に有効です。JestやMochaに慣れている方も、構文が似ているためすぐに使い始められるでしょう。
参考リンク#