C.W.K.
Stream
Lesson 02 of 03 · published

test.each 로 Parametrized 테스트

~14 min · vitest-advanced, parametrized, test.each

Level 0테스트 호기심
0 XP0/32 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete
"테스트 하나, row 여러 개. 케이스 추가가 한 줄이지 열 줄 아냐."

Parametrized 테스트가 푸는 문제

입력을 출력으로 매핑하는 함수 테스트 중 — 파서, 산술 헬퍼, 검증기, 포매터. 자연스러운 테스트 방식은 테이블:

  • sum(1, 2)3
  • sum(-1, 1)0
  • sum(0, 0)0
  • sum(1.5, 2.5)4

Parametrization 없으면 거의 동일한 본문의 it() 블록 네 개. 다섯 번째 케이스 추가가 열 줄. test.each (it.each 로도 노출) 로 테이블이 데이터고 테스트 정의가 row 당 한 번 돌아.

두 문법 — 배열의 배열 vs Tagged 템플릿

Vitest 가 테이블 표현하는 동등한 두 방식 노출:

배열의 배열 — 간결, 프로그램적, 계산 쉬움:

test.each([[1, 2, 3], [-1, 1, 0]])('sum(%i, %i) = %i', (a, b, expected) => { ... });

Tagged 템플릿 — 스프레드시트처럼 읽힘, 컬럼 많거나 가독성에 더 좋음:

test.each`
  a    | b    | expected
  ${1} | ${2} | ${3}
  ${-1}| ${1} | ${0}
`('sum($a, $b) = $expected', ({ a, b, expected }) => { ... });

테스트의 중심에 따라 선택: 숫자와 단순 값 → 배열 형식; 명확한 의미 가진 명명 컬럼 → 템플릿 형식.

생성된 테스트 이름 — 써

test.each 의 첫 인자는 printf 스타일 placeholder (%i, %s, 객체는 %o) 나 템플릿 $name 참조 가진 템플릿 문자열. 생성된 이름이 테스트 리포트에 나타나는 거. 좋은 이름은 다섯 줄 실패 출력을 이야기로 바꿔:

"sum(-1, 1) = 0" failed: expected 0 but received -2

vs 나쁜 이름 버전:

"sum works" failed: expected 0 but received -2

첫 번째는 어느 row 가 깨졌는지 정확히 말해줘. 두 번째는 테스트 본문 읽게 강요.

Parametrized 테스트는 케이스가 데이터에서만 다를 때 맞아. 케이스가 setup, mock, 단언 모양에서 다르면 테이블 아냐 — 하나인 척하는 별개 테스트 셋. 신호: 본문에서 row 기반 분기하기 시작하면 멈추고 쪼개.

Parametrize 하지 말아야 할 때

  • 케이스 두 개. it 블록 두 개가 두 row 테이블보다 명확.
  • 테스트 본문이 구조적으로 발산 — 케이스별 다른 mock, 케이스별 다른 단언. 한 테이블에 강제하면 테스트 읽기 더 어려워.
  • 입력이 크거나 복잡 (깊은 객체, fixture). 템플릿이 '한눈에 스프레드시트' 품질 잃어.

Code

배열 형식 — 최소 의식·typescript
// Array form — terse, easy to compute table contents
import { describe, test, expect } from 'vitest';
import { sum } from './math';

describe('sum (parametrized)', () => {
  test.each([
    [1, 2, 3],
    [-1, 1, 0],
    [0, 0, 0],
    [1.5, 2.5, 4],
    [Number.MAX_SAFE_INTEGER, 1, Number.MAX_SAFE_INTEGER + 1],
  ])('sum(%i, %i) = %i', (a, b, expected) => {
    expect(sum(a, b)).toBe(expected);
  });
});
Tagged 템플릿 형식 — 테이블처럼 읽힘·typescript
// Tagged template form — reads like a spreadsheet
import { describe, test, expect } from 'vitest';
import { formatCurrency } from './format';

describe('formatCurrency (table)', () => {
  test.each`
    amount     | currency | expected
    ${0}       | ${'USD'} | ${'$0.00'}
    ${1}       | ${'USD'} | ${'$1.00'}
    ${1234.5}  | ${'USD'} | ${'$1,234.50'}
    ${-99.99}  | ${'USD'} | ${'-$99.99'}
    ${1000}    | ${'EUR'} | ${'€1,000.00'}
    ${1000}    | ${'JPY'} | ${'¥1,000'}
  `(
    'formatCurrency($amount, $currency) returns $expected',
    ({ amount, currency, expected }) => {
      expect(formatCurrency(amount as number, currency as string)).toBe(expected);
    }
  );
});
프로그램적 row — 케이스가 데이터 소스에서 올 때·typescript
// Generating rows programmatically — when the table itself comes from data
import { describe, test, expect } from 'vitest';
import { isValidEmail } from './validation';

const validEmails = [
  'simple@example.com',
  'user.name+tag@example.co.uk',
  'a@b.io',
];

const invalidEmails = [
  '',
  'no-at-sign',
  '@no-local-part.com',
  'no-domain@',
  'spaces in@email.com',
];

describe('isValidEmail', () => {
  test.each(validEmails)('accepts "%s"', (email) => {
    expect(isValidEmail(email)).toBe(true);
  });

  test.each(invalidEmails)('rejects "%s"', (email) => {
    expect(isValidEmail(email)).toBe(false);
  });
});
describe.each — 같은 suite, 여러 타겟·typescript
// describe.each — when whole describe blocks differ by parameter
import { describe, test, expect } from 'vitest';
import { createReducer } from './reducer';

// Run the same suite for every reducer variant.
describe.each([
  ['counter', createReducer('counter'), 0],
  ['toggle', createReducer('toggle'), false],
  ['list', createReducer('list'), []],
])('%s reducer', (name, reducer, initial) => {
  test('returns the initial state for an unknown action', () => {
    expect(reducer(initial as any, { type: 'UNKNOWN' })).toEqual(initial);
  });

  test('throws on a null action', () => {
    expect(() => reducer(initial as any, null as any)).toThrow();
  });
});

External links

Exercise

Track 2 assertion lesson 의 divide 테스트를 단일 test.each 테이블로 다시 짜. Happy path (10/2, 7/2, 0/10) 와 throw 케이스 (5/0 — expected 가 'throws' 문자열인 row 써서 본문에서 switch, OR throw row 만 위한 별개 test.each 써). 원본과 가독성과 라인 수 비교.
Hint
Throw 케이스 다르게 처리하려고 본문에서 분기하면, 신호야 — happy path 용 parametrized 테스트 하나와 에러 케이스용 하나로 쪼개.

Progress

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

댓글 0

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

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