매처로 값 검증하기

매처로 값 검증하기 (Using Matchers)

expect 에 '매처(matcher)'를 붙여 값이 어떤 조건을 만족하는지 단언하는 게 Vitest 의 핵심 사용법이에요. 이 페이지에서는 실제로 자주 쓰이는 매처들을 소개할게요. 완전한 목록이 필요하면 Expect API 레퍼런스를 참고하세요. 매처를 고르는 기준은 '내가 확인하려는 조건을 가장 정확히 표현하는 것'이에요 — 의미가 애매하면 버그를 놓치기 쉬우니까요.

출처: Vitest — Using Matchers

자주 쓰는 매처

값을 테스트하는 가장 단순한 방법은 정확한 동일성(equality) 검사예요. expect(2 + 2).toBe(4) 라고 쓰면 toBe 매처가 그 값이 정확히 4 인지 Object.is 로 확인해요.

import { expect, test } from 'vitest'

test('two plus two is four', () => {
  expect(2 + 2).toBe(4)
})

숫자·문자열·불리언 같은 원시값에는 이 방식이 잘 맞아요. 그런데 객체를 비교할 때 toBe동일성을 봐요. 즉 메모리상의 정확히 같은 객체인지 보지, 같은 모양인지는 보지 않아요. 그런 비교는 toEqual 몫이에요. 객체의 모든 필드나 배열의 모든 요소를 재귀적으로 비교하되, 객체 동일성은 무시해요:

test('object assignment', () => {
  const data = { one: 1 }
  data.two = 2

  expect(data).toEqual({ one: 1, two: 2 })
})

차이가 더 잘 드러나는 예시를 볼게요. 내용이 같은 두 객체는 toEqual 이지만 toBe 는 아니에요:

test('toBe vs toEqual', () => {
  const a = { name: 'Alice' }
  const b = { name: 'Alice' }

  // These are different objects in memory
  expect(a).not.toBe(b)

  // But they have the same structure
  expect(a).toEqual(b)
})

toStrictEqualtoEqual 보다 세 가지 면에서 더 엄격해요. undefined 프로퍼티를 검사하고, 희소 배열(sparse array)과 undefined 값을 구분하며, 객체의 타입까지(모양뿐 아니라) 확인해요:

test('toEqual vs toStrictEqual', () => {
  // toEqual ignores undefined properties
  expect({ a: 1 }).toEqual({ a: 1, b: undefined })

  // toStrictEqual catches them
  expect({ a: 1 }).not.toStrictEqual({ a: 1, b: undefined })

  // toEqual doesn't check object types
  class User {
    constructor(name) {
      this.name = name
    }
  }
  expect(new User('Alice')).toEqual({ name: 'Alice' })
  expect(new User('Alice')).not.toStrictEqual({ name: 'Alice' })
})

::: tip 경험칙 하나를 정리하면 이래요. 원시값(숫자·문자열·불리언)에는 toBe, 구조 비교에는 toEqual, 타입이나 명시적 undefined 값까지 신경 써야 할 때는 toStrictEqual 을 쓰면 돼요. :::

어떤 매처든 앞에 .not 을 붙이면 부정할 수 있어요. '~가 아님'을 확인하고 싶을 때 유용하죠:

test('adding positive numbers is not zero', () => {
  expect(1 + 2).not.toBe(0)
})

Truthiness

테스트를 하다 보면 undefined, null, false 를 구분해야 할 때가 있고, 정확한 값이 아니라 그냥 truthy 인지 falsy 인지만 알고 싶을 때도 있어요. Vitest 는 두 상황 모두에 매처를 제공해요:

  • toBeNullnull 만 매칭
  • toBeUndefinedundefined 만 매칭
  • toBeDefinedtoBeUndefined 의 반대. undefined 가 아닌 모든 값에 통과
  • toBeTruthyif 문이 참으로 취급할 모든 것에 매칭
  • toBeFalsyif 문이 거짓으로 취급할 모든 것에 매칭

확인하려는 대상을 가장 정확히 설명하는 매처를 골라야 해요. 진짜로 원하는 게 toBeDefined 인데 toBeTruthy 를 쓰면 버그를 숨길 수 있어요. 0"" 은 둘 다 defined 이면서 falsy 이니까요.

test('null checks', () => {
  const n = null

  expect(n).toBeNull()
  expect(n).toBeDefined()
  expect(n).toBeFalsy()
  expect(n).not.toBeTruthy()
  expect(n).not.toBeUndefined()
})

test('zero', () => {
  const z = 0

  expect(z).toBeDefined() // passes: 0 is defined
  expect(z).toBeFalsy() // passes: 0 is falsy
  expect(z).not.toBeNull() // passes: 0 is not null
})

숫자

대부분의 숫자 비교는 직관적이에요. Vitest 는 크다, 작다, 같다 검사에 기대하는 매처를 그대로 제공해요:

test('number comparisons', () => {
  const value = 2 + 2

  expect(value).toBeGreaterThan(3)
  expect(value).toBeGreaterThanOrEqual(3.5)
  expect(value).toBeLessThan(5)
  expect(value).toBeLessThanOrEqual(4.5)

  // For exact equality, both toBe and toEqual work the same for numbers
  expect(value).toBe(4)
  expect(value).toEqual(4)
})

부동소수점 산술에는 자주 걸리는 함정이 하나 있어요. JavaScript 에서 0.1 + 0.2 는 정확히 0.3 이 아니에요(0.30000000000000004). 그래서 toBe(0.3) 검사는 실패해요. 대신 toBeCloseTo 를 쓰세요. 작은 반올림 오차 안에서 숫자를 비교해줘요:

test('adding floating point numbers', () => {
  const value = 0.1 + 0.2

  // This won't work because of floating point rounding
  // expect(value).toBe(0.3)

  // This works
  expect(value).toBeCloseTo(0.3)
})

문자열

toMatch 로 문자열을 정규식에 대조할 수 있어요. 정확한 값보다 패턴이 중요할 때, 예를 들어 에러 메시지에 특정 단어가 들어있는지나 URL 이 특정 형식을 따르는지 확인할 때 특히 유용해요:

test('there is no I in team', () => {
  expect('team').not.toMatch(/I/)
})

test('version string matches semver format', () => {
  expect('[email protected]').toMatch(/vitest@\d+\.\d+\.\d+/)
})

배열과 이터러블

toContain 은 배열(또는 Set 같은 이터러블)에 특정 항목이 들어있는지 확인해요. 비교에 === 를 쓰기 때문에 원시값에는 잘 맞아요:

test('the shopping list has milk in it', () => {
  const shoppingList = ['milk', 'bread', 'eggs', 'butter']

  expect(shoppingList).toContain('milk')
  expect(new Set(shoppingList)).toContain('milk')
})

배열이 특정 구조의 객체를 포함하는지 확인해야 한다면 toContainEqual 을 쓰면 돼요. 배열 안의 개별 항목에 대해 toEqual 처럼 동작하죠.

객체

객체를 테스트할 때 모든 프로퍼티를 전부 명시하고 싶지 않고 중요한 필드 몇 개만 확인하고 싶은 경우가 많아요. toMatchObject 는 정확히 그런 일을 해줘요. 지정한 프로퍼티를 최소한 포함하는지만 확인하고, 추가 프로퍼티는 무시해요:

test('user has expected fields', () => {
  const user = {
    id: 1,
    name: 'Alice',
    email: '[email protected]',
    createdAt: '2024-01-01'
  }

  // We only care about name and email here
  expect(user).toMatchObject({
    name: 'Alice',
    email: '[email protected]',
  })
})

개별 프로퍼티, 특히 중첩된 프로퍼티를 확인할 때는 toHaveProperty 가 더 읽기 좋아요. 점으로 구분한 경로와, 선택적으로 기대값을 넘기면 돼요:

test('object has property', () => {
  const user = {
    name: 'Alice',
    address: { city: 'Paris', zip: '75001' }
  }

  expect(user).toHaveProperty('name')
  expect(user).toHaveProperty('name', 'Alice')
  expect(user).toHaveProperty('address.city', 'Paris')
  expect(user).toHaveProperty('address.zip')
})

비대칭 매처 (Asymmetric Matchers)

정확한 값은 모르지만 타입이나 모양은 아는 경우가 있어요. 비대칭 매처를 쓰면 정확한 내용을 못 박지 않고 '이런 모양이어야 한다'고 서술할 수 있어요. toEqual 이나 toMatchObject 처럼 깊은 비교를 하는 매처 안에서 그대로 동작해요:

test('user has the right shape', () => {
  const user = createUser('Alice')

  expect(user).toEqual({
    id: expect.any(Number),
    name: 'Alice',
    email: expect.stringContaining('@'),
    roles: expect.arrayContaining(['viewer']),
  })
})

가장 흔한 비대칭 매처들은 이래요:

예외 (Exceptions)

함수가 에러를 던지는지 확인하려면 toThrow 를 써요. 호출을 다른 함수로 감싸야 하는데, 그래야 Vitest 가 에러를 잡아서 테스트를 깨뜨리지 않게 할 수 있어요:

function compileCode(code) {
  if (code === '') {
    throw new Error('Cannot compile empty string')
  }
  return code
}

test('compiling an empty string throws', () => {
  // Check that it throws at all
  expect(() => compileCode('')).toThrow()

  // Check the error message
  expect(() => compileCode('')).toThrow('Cannot compile empty string')

  // Check the message with a regex
  expect(() => compileCode('')).toThrow(/empty string/)
})

::: tip () => compileCode('') 처럼 감싸는 함수가 중요해요. expect(compileCode('')).toThrow() 로 쓰면 expect 가 잡기 전에 에러가 던져져서, 테스트가 처리되지 않은 에러로 실패해 버려요. :::

소프트 단언 (Soft Assertions)

보통 실패한 단언은 그 자리에서 테스트를 멈춰요. 대부분은 이게 유용하지만, 서로 독립적인 여러 가지를 한꺼번에 확인하고 싶어서 실패를 하나씩 고치기보다 한 번에 다 보고 싶을 때가 있어요. expect.soft 는 정확히 그렇게 해요. 실패를 기록하되 테스트는 계속 진행시켜요:

test('check multiple fields', () => {
  const user = { name: 'Alice', age: 30, role: 'admin' }

  expect.soft(user.name).toBe('Alice')
  expect.soft(user.age).toBe(25) // this fails but execution continues
  expect.soft(user.role).toBe('admin')
  // the test report will show that age didn't match
})

API 응답이나 복잡한 객체의 모양을 검증할 때 특히 유용해요. 여러 필드가 동시에 틀려 있을 수 있으니까요.

더 알아보기 (Learn more)