본문 바로가기
C.W.K.
Stream
Lesson 01 of 05 · published

node --test — 내장 테스트 러너

~12 min · modern-node, testing, node-test

Level 0노드 입문자
0 XP0/40 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"예전에는 Node project를 시작하면 Jest나 Mocha부터 넣었어. 지금은 runtime 안에 test runner가 있으니, 외부 도구가 필요한 이유부터 확인할 차례야."

설치 없이 바로 시작해

Node 20 이상에는 node:test가 들어 있어. node --test를 실행하면 정해진 이름의 test file을 찾아 돌려 주는 완전한 runner야. Package를 설치하거나 별도 설정 file을 만들지 않아도 되고, Node가 직접 이해하는 code라면 test만을 위한 transpile 단계도 필요 없어.

// test/add.test.mjs
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { add } from '../src/math.mjs';

test('add returns the sum of two numbers', () => {
  assert.equal(add(1, 2), 3);
});

test('add handles negatives', () => {
  assert.equal(add(-1, 1), 0);
});

기본 출력은 TAP 형식이고, 사람이 읽기 편한 spec 형식을 원하면 --test-reporter=spec을 붙이면 돼.

작은 runner라고 기능까지 작은 건 아니야

  • describe(name, fn)으로 관련 test를 묶을 수 있어.
  • before, after, beforeEach, afterEach hook을 제공해.
  • test.skip은 실행을 건너뛰고, test.todo는 아직 구현하지 않은 test를 표시해.
  • test.only--test-only를 함께 쓰면 표시한 test에 집중할 수 있어.
  • Test 안에서 t.test(...)를 불러 하위 test를 만들 수 있어.
  • Async function을 그대로 test body로 쓰거나 context t를 받는 callback을 쓸 수 있어.

Mocha를 써 봤다면 낯설지 않은 모양이야. Coverage는 node --test --experimental-test-coverage로 수집할 수 있고, 새 Node에서는 안정화된 동등 기능을 확인해 쓰면 돼.

일반적인 mock도 내장돼 있어

node:test는 test context의 t.mock을 통해 method와 timer를 대신할 수 있어.

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

import * as db from '../src/db.mjs';

test('fetches user from db', (t) => {
  const stub = t.mock.method(db, 'findUser', () => ({
    id: 1, name: 'Pippa'
  }));

  const result = getUser(1);
  assert.equal(result.name, 'Pippa');
  assert.equal(stub.mock.calls.length, 1);
});

t.mock.timers.enable()로 시간도 제어할 수 있어. Jest보다 API 범위는 작지만 service test의 흔한 stub과 timer 제어에는 충분해. Snapshot이나 복잡한 fixture가 핵심인 project라면 Jest를 유지하고, 그렇지 않다면 내장 runner부터 검토해.

변경할 때마다 다시 돌리기

node --test --watch는 관련 file이 바뀌면 test를 다시 실행해. Server용 node --watch와 같은 runtime primitive라 test 재실행만을 위해 nodemon을 하나 더 둘 필요가 없어.

Jest를 떠날 때 잃는 기능도 계산해

  • 전용 snapshot test API
  • expect(x).toEqual({...}) 같은 풍부한 matcher와 부분 deep matching
  • jest.mock('./module')처럼 import 자체를 다시 쓰는 자동 module mocking
  • Watch mode에서 원하는 test를 고르는 interactive UI

Project가 이 기능을 실제로 쓴다면 Jest가 맞아. 반대로 test 대부분이 assertion과 method mock 몇 개라면 node:test로 옮겨 devDependencies에서 큰 tree를 덜어 낼 수 있어. Dependency 크기보다 중요한 건 test code가 어느 runner의 고유 기능에 기대고 있는지 먼저 알아내는 거야.

Pippa의 고백

한 frontend project에는 Vitest가 들어 있었고 node_modules에서 차지하는 몫도 컸어. 설치 시간을 살피다가 test runner가 주요 원인 중 하나라는 걸 봤지. 내장 mock으로 옮길 수 있는 test를 바꾸자 coverage는 유지하면서 dependency tree와 설치 시간이 줄었어. 그때 아빠가 남긴 기준이 딱 맞았어. “Vitest는 그 기능이 필요해서 써. 기본이라서 쓰는 게 아니야.” 지금은 node:test에서 시작하고, Jest 계열 기능이 실제로 필요할 때만 더 큰 runner를 골라.

Code

Server를 띄워 실제 request를 검사하는 test·javascript
// Hook, async test, 하위 test를 함께 사용
import { test, describe, before, after } from 'node:test';
import assert from 'node:assert/strict';
import { Server } from '../src/server.mjs';

describe('Server', () => {
  let server;
  before(async () => { server = await Server.start({ port: 0 }); });
  after(async () => { await server.stop(); });

  test('GET / returns hi', async () => {
    const res = await fetch(`http://localhost:${server.port}/`);
    assert.equal(res.status, 200);
    assert.equal(await res.text(), 'hi');
  });

  test('POST /echo round-trips', async (t) => {
    await t.test('with JSON', async () => {
      const res = await fetch(`http://localhost:${server.port}/echo`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: '{"a":1}',
      });
      assert.deepEqual(await res.json(), { a: 1 });
    });
  });
});
자주 쓰는 node --test option·bash
# ./test 아래의 test를 모두 실행
node --test

# File 하나 또는 pattern에 맞는 file만 실행
node --test test/add.test.mjs
node --test test/**/*.test.mjs

# File이 바뀌면 다시 실행
node --test --watch

# Spec 형식으로 출력
node --test --test-reporter=spec

# test.only로 표시한 것만 실행
node --test --test-only

# Coverage 수집
node --test --experimental-test-coverage

# 지울 수 있는 문법만 쓴 .ts test도 직접 실행
node --test test/**/*.test.ts

External links

Exercise

테스트가 100개보다 적은 작은 Jest, Vitest, 또는 Mocha 프로젝트를 골라 node:test로 옮겨 봐. 이전 전후의 설치 시간과 node_modules 크기를 재고, 내장 실행기로 재현하지 못한 기능이나 API 차이 때문에 다시 작성한 테스트를 따로 기록해. 마지막에는 단순히 옮겼다고 끝내지 말고, 기존 테스트 모음이 어느 실행기 고유 기능에 기대고 있었는지 설명해.
Hint
표면적인 변환은 expect(a).toBe(b)assert.equal(a, b)로, jest.fn()t.mock.fn()으로 바꾸는 식이야. 스냅숏과 모듈 모킹은 깔끔하게 옮겨지지 않는 경우가 많아. jest.mock('./db')가 필요했다면 모듈 전체를 바꾸는 일이 정말 필요한지, 의존성 주입으로 경계를 단순하게 만들 수 있는지도 함께 검토해.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고
💛 by 똘이warm

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.