스냅샷 테스트

스냅샷 테스트 (Snapshot Testing)

스냅샷 테스트는 어떤 코드의 출력을 포착해서 파일로 저장하고, 이후 실행 때마다 그 출력을 저장된 스냅샷과 비교해요. 출력이 바뀌면 테스트가 실패하죠. 그 변경이 버그이거나, 아니면 스냅샷을 갱신해야 하는 상황이에요. 특히 구조화된 출력을 만드는 것을 테스트할 때 유용해요 — 복잡한 객체를 반환하는 함수, HTML 을 렌더링하는 컴포넌트, 여러 줄 메시지를 만드는 에러 포매터 같은 것들요. 필드마다·줄마다 수동 단언을 쓰면 지루하고 깨지기 쉬우니, 출력 전체를 한 번 포착해 두고 Vitest 가 바뀌는지 알려주게 하는 거예요.

출처: Vitest — Snapshot Testing

첫 번째 스냅샷

스냅샷 테스트를 만들려면 값을 toMatchSnapshot() 에 넘기면 돼요:

import { expect, test } from 'vitest'

function generateGreeting(name) {
  return {
    message: `Hello, ${name}!`,
    timestamp: null,
    version: 2,
  }
}

test('generates a greeting', () => {
  expect(generateGreeting('Alice')).toMatchSnapshot()
})

이 테스트를 처음 실행하면 비교할 스냅샷이 없으니 Vitest 가 하나를 만들어요. 테스트 파일 옆 __snapshots__ 디렉터리에 저장하죠:

__snapshots__/
  example.test.js.snap

그 파일을 열어 보면 값의 직렬화된 표현이 보여요:

exports['generates a greeting 1'] = `
{
  "message": "Hello, Alice!",
  "timestamp": null,
  "version": 2,
}
`

이제 이 테스트를 실행할 때마다 Vitest 는 generateGreeting('Alice') 의 출력을 직렬화해서 저장된 스냅샷과 문자 단위로 비교해요. 출력이 바뀌면(누군가 메시지 형식을 수정하거나 버전 번호를 올리면) 테스트가 실패하면서 무엇이 바뀌었는지 명확한 diff 를 보여줘요.

::: tip 스냅샷 파일은 버전 관리를 하세요. 기대 출력의 기록 역할을 하고, 다른 테스트 단언과 마찬가지로 코드 리뷰에서 검토돼야 하니까요. :::

인라인 스냅샷 (Inline Snapshots)

외부 스냅샷 파일은 잘 동작하지만, 기대 출력이 실제로 어떤지 보려면 다른 파일로 이동해야 한다는 단점이 있어요. 값이 작다면 toMatchInlineSnapshot() 으로 스냅샷을 테스트 파일 안에 바로 두는 게 더 편해요.

인자 없이 단언부터 작성해요:

test('generates a greeting', () => {
  expect(generateGreeting('Alice')).toMatchInlineSnapshot()
})

테스트를 실행하면 Vitest 가 자동으로 스냅샷을 문자열 인자로 채워 넣어요:

test('generates a greeting', () => {
  expect(generateGreeting('Alice')).toMatchInlineSnapshot(`
    {
      "message": "Hello, Alice!",
      "timestamp": null,
      "version": 2,
    }
  `)
})

이제 기대 출력이 그걸 만들어내는 코드 바로 옆에 있어요. 테스트만 읽어도 generateGreeting 이 무엇을 반환해야 하는지 바로 이해할 수 있고, 출력이 바뀌면 Vitest 가 그 자리의 문자열을 갱신하니 별도 스냅샷 파일을 관리할 필요가 없어요.

인라인 스냅샷은 작고 집중된 값에 좋아요. 큰 출력(전체 HTML 페이지 같은)에는 외부 스냅샷이나 파일 스냅샷이 더 어울려요.

::: tip 외부 스냅샷과 달리 인라인 스냅샷은 별도 .snap 파일을 만들지 않아요. 기대값이 toMatchInlineSnapshot() 의 인자로 테스트 파일에 바로 저장되니, 커밋할 게 따로 없죠. :::

스냅샷 갱신하기

의도적으로 코드의 출력을 바꾸면 기존 스냅샷은 낡아지고 테스트는 실패할 거예요. 이건 설계상 의도된 동작이에요. 스냅샷 테스트의 핵심 목적 자체니까요. 하지만 새 출력이 올바르다는 걸 확인했으면 스냅샷을 갱신해야 해요.

갱신하는 방법은 몇 가지가 있어요:

  • 감시 모드(watch mode) — 터미널에서 u 를 눌러 실패한 스냅샷 전부 갱신
  • CLIvitest -uvitest --update 실행으로 갱신 후 종료
  • VS CodeVitest 확장의 테스트 거터(gutter) 아이콘에서 "Update Snapshots" 명령 사용
vitest -u

인라인 스냅샷은 Vitest 가 새 값으로 테스트 파일을 직접 수정하고, 외부 스냅샷은 .snap 파일을 다시 써요.

::: warning 스냅샷을 갱신할 땐 조심하세요. 항상 diff 를 검토해서 변경이 의도된 것인지, 버그가 아닌지 확인해야 해요. u 를 무턱대고 누르다 보면 망가진 출력을 실수로 받아들이기 쉬워요. :::

파일 스냅샷 (File Snapshots)

때로는 테스트할 출력이 너무 커서 외부 .snap 파일조차 어색하게 느껴지거나, 에디터에서 문법 하이라이팅이 제대로 된 스냅샷을 보고 싶을 때가 있어요. toMatchFileSnapshot() 은 스냅샷을 원하는 확장자의 파일로 저장하게 해줘요:

test('renders the component', async () => {
  const html = renderComponent()
  await expect(html).toMatchFileSnapshot('./fixtures/component.html')
})

스냅샷은 평범한 .html 파일로 저장돼서 브라우저에서 열거나, 문법 하이라이팅으로 보거나, 표준 도구로 diff 할 수 있어요. HTML, SVG, CSS, 생성된 코드, 또는 파일 형식이 가독성에 중요한 어떤 출력이든 잘 맞아요.

스냅샷은 언제 쓸까

스냅샷은 구조화되고 직렬화 가능한 출력을 수동으로 단언하긴 고통스러울 때 빛을 발해요. 흔한 사례 몇 가지:

  • 중첩된 필드가 많은 복잡한 설정 객체를 반환하는 함수
  • 렌더링 함수나 템플릿 엔진이 만드는 HTML 이나 마크업
  • 포맷된 스택 트레이스나 컨텍스트 정보를 포함한 에러 메시지
  • 특정 포매팅을 가진 CLI 출력이나 로그 메시지
  • 예상치 못한 필드 변경을 잡고 싶은 JSON API 응답

반대로 스냅샷이 항상 최고의 도구는 아니에요. 출력이 자주 바뀐다면(타임스탬프나 무작위 ID 를 포함한다든지) 스냅샷을 갱신하는 시간이 절약해 주는 시간보다 커져요. 그리고 딱 한두 개의 특정 필드만 신경 쓴다면, toMatchObjecttoHaveProperty 같은 정밀한 단언이 모든 것을 포착하는 스냅샷보다 의도를 더 잘 드러내요.

일반 규칙을 정리하면 이래요. 출력의 어떤 변경에도 대비하고 싶을 때는 스냅샷을, 특정 프로퍼티에만 관심이 있을 때는 정밀한 단언을 쓰세요.

동적 값 다루기

타임스탬프나 ID 처럼 실행마다 바뀌는 값이 출력에 포함된다면, 프로퍼티 매처로 구조는 고정하면서 변동이 심한 필드는 무시할 수 있어요. toMatchSnapshot() 이나 toMatchInlineSnapshot() 의 첫 인자로 비대칭 매처가 든 객체를 넘기면 돼요:

test('user snapshot with dynamic fields', () => {
  const user = createUser('Alice')

  expect(user).toMatchSnapshot({
    id: expect.any(Number),
    createdAt: expect.any(Date),
  })
})

idcreatedAt 필드는 저장된 값과 비교되는 대신 매처(어떤 숫자, 어떤 날짜)로 확인돼요. 나머지 필드는 평소처럼 스냅샷 처리되고요.

에러 스냅샷 (Error Snapshots)

인라인 스냅샷의 흔한 용도 중 하나가 에러 메시지를 포착하는 거예요. toThrowErrorMatchingInlineSnapshottoThrowtoMatchInlineSnapshot 을 합쳐서, 별도 .snap 파일 없이 에러 메시지를 스냅샷할 수 있게 해줘요:

test('throws on invalid input', () => {
  expect(() => parse('')).toThrowErrorMatchingInlineSnapshot(
    `[Error: Unexpected end of input at position 0]`
  )
})

에러 메시지가 명확한지, 그리고 실수로 바뀌지 않는지 확인할 때 특히 유용해요. 다른 인라인 스냅샷처럼 첫 실행에 문자열을 채워 넣고 u 를 누르면 갱신돼요.

::: tip 커스텀 스냅샷 직렬라이저·스냅샷 매처·고급 설정은 Snapshot 가이드를 참고하세요. :::

더 알아보기 (Learn more)