테스트 작성하기

테스트 작성하기 (Writing Tests)

Getting Started 가이드에서 Vitest 를 설치하고 첫 테스트를 돌려봤다면, 이제 테스트를 어떻게 구성하고 정리하는지 더 깊게 볼 차례예요. 이 페이지는 testexpect 를 쓰는 기본 흐름부터 describe 로 테스트를 묶는 법, TypeScript 테스트, 건너뛰기·집중 실행, 그리고 같은 테스트를 여러 입력으로 돌리는 파라미터화까지 한 번에 다뤄요. 코드 예시를 그대로 따라 해 보면 테스트 파일이 어떤 모양을 갖는지 금방 감이 잡힐 거예요.

출처: Vitest — Writing Tests

첫 테스트 작성하기

테스트는 '특정 코드가 예상한 결과를 내는지'를 검증해요. Vitest 에서는 test 함수로 테스트를 정의하고, expect 로 단언(assertion)을 만들어요. 각 테스트는 이름(무엇을 확인하는지 설명하는 문자열)과 하나 이상의 단언을 담은 함수로 이루어져요. 단언이 하나라도 실패하면 그 테스트는 실패로 처리되고요.

import { expect, test } from 'vitest'

test('Math.sqrt works for perfect squares', () => {
  expect(Math.sqrt(4)).toBe(2)
  expect(Math.sqrt(144)).toBe(12)
  expect(Math.sqrt(0)).toBe(0)
})

::: details test 를 쓸까요, it 을 쓸까요? 가끔 test 대신 it 으로 작성된 테스트를 보게 될 거예요. 둘은 완전히 똑같이 동작해요. it 은 단지 별칭인데, 설명적인 이름과 자연스럽게 어울린다고 좋아하는 사람들이 있어요:

import { expect, it } from 'vitest'

it('should compute square roots', () => {
  expect(Math.sqrt(4)).toBe(2)
})

어느 쪽이든 취향대로 쓰면 돼요. 동작은 같고 한 프로젝트 안에서 섞어도 문제없어요. 코드베이스 전반에 걸쳐 하나로 통일하고 싶다면 consistent-test-it ESLint 규칙(oxlint 에도 있어요)이 도움이 돼요. :::

describe 로 테스트 묶기

테스트 파일이 커지면 관련된 테스트끼리 묶고 싶어져요. describe 는 이름 붙은 테스트 그룹, 즉 테스트 스위트를 만들어요:

import { describe, expect, test } from 'vitest'

describe('Math.sqrt', () => {
  test('returns the square root of perfect squares', () => {
    expect(Math.sqrt(4)).toBe(2)
    expect(Math.sqrt(9)).toBe(3)
  })

  test('returns NaN for negative numbers', () => {
    expect(Math.sqrt(-1)).toBeNaN()
  })

  test('returns 0 for 0', () => {
    expect(Math.sqrt(0)).toBe(0)
  })
})

describe 블록은 여러 겹으로 중첩할 수 있지만, 너무 깊게 쌓지는 않는 게 좋아요. 중첩이 깊어지면 테스트를 읽기 어려워지거든요. 단순한 모듈은 테스트를 평평하게 나열하는 것만으로 충분할 때가 많고, 파일이 여러 함수나 메서드를 테스트할 때 describe 가 빛을 발해요.

테스트 파일

기본적으로 Vitest 는 이름에 .test. 이나 .spec. 이 들어가는 파일을 찾아요. utils.test.js, app.spec.js, math.test.jsx 같은 파일이죠. 모든 하위 디렉터리를 다 훑기 때문에 파일을 어디에 두든 상관없어요.

정확한 패턴은 이 두 가지예요:

  • **/*.test.{ts,js,mjs,cjs,tsx,jsx}
  • **/*.spec.{ts,js,mjs,cjs,tsx,jsx}

테스트 파일을 '이렇게' 놓으라는 단 하나의 정답은 없어요. 어떤 팀은 테스트 대상 소스 바로 옆에 두는 걸 선호하고, 어떤 팀은 별도 디렉터리에 모아두죠. Vitest 는 어느 쪽이든 다 찾아내요:

src/
  utils.js
  utils.test.js       # co-located with the source
  __tests__/
    utils.test.js      # in a test directory

기본 패턴이 프로젝트에 안 맞는다면 includeexclude 설정으로 어떤 파일을 테스트 대상에 넣을지 직접 바꿀 수 있어요.

TypeScript 테스트하기

Vitest 는 Vite 위에서 돌아가기 때문에 TypeScript 가 별다른 설정 없이 그대로 동작해요. 별도 컴파일러를 설치할 일도, ts-jest 를 설정할 일도, 테스트를 위한 빌드 단계도 없어요. 테스트 파일 이름을 .test.js 대신 .test.ts 로 붙이기만 하면 바로 쓸 수 있어요:

import { expect, test } from 'vitest'

interface User {
  name: string
  age: number
}

function createUser(name: string, age: number): User {
  return { name, age }
}

test('creates a user with the correct fields', () => {
  const user = createUser('Alice', 30)

  expect(user).toEqual({ name: 'Alice', age: 30 })
  expect(user.name).toBe('Alice')
})

프로덕션 타입을 import 해서 쓰거나, 제네릭을 쓰거나, 타입이 있는 테스트 유틸을 만드는 것 모두 코드베이스의 나머지 코드와 똑같이 할 수 있어요. Vite 가 TypeScript 를 즉석에서 변환하니 큰 프로젝트에서도 테스트 시작이 빨라요.

::: tip Vitest 는 실행을 위해 TypeScript 를 변환하지만, 테스트 실행 중에 타입 검사는 하지 않아요. 빠른 피드백을 위한 Vite 의 트레이드오프와 같은 이유죠. 터미널에선 빠른 피드백을 받고, 전체 타입 검사가 필요할 땐 tscvitest typecheck 를 따로 돌리면 돼요. 자세한 내용은 Testing Types 가이드를 참고하세요. :::

테스트 출력 읽기

vitest 를 실행했을 때 테스트 파일이 하나만 걸리면, 출력은 describe 그룹과 개별 테스트, 그리고 각각의 소요 시간을 보여주는 트리 구조로 펼쳐져요. 테스트 파일이 여러 개 돌면 Vitest 는 파일 하나를 한 줄로 접어서 출력을 관리하기 쉽게 만들어요. 테스트가 실패하면 기대값, 실제값, 그 차이를 강조한 diff, 그리고 실패한 단언 주변 코드 스니펫이 함께 표시돼요. 파일과 줄 번호도 포함돼서 바로 소스로 건너뛸 수 있고요. diff 와 코드 스니펫만 봐도 대부분은 파일을 열거나 console.log 를 추가하지 않고도 무엇이 잘못됐는지 알 수 있어요.

테스트 건너뛰기와 집중하기

개발하는 동안 전체 테스트 중 일부만 돌리고 싶은 때가 많아요. Vitest 는 이럴 때 쓰는 수식어를 제공해요:

.only 는 이 테스트(또는 스위트)만 실행하고 파일 안의 나머지는 전부 건너뛰라고 알려줘요. 특정 테스트에 집중하고 싶은데 전체 스위트가 끝나길 기다리기 싫을 때 유용해요:

test.only('focus on this test', () => {
  // only this test runs in the file
})

.skip 은 그 반대예요. 지우지 않고 테스트를 건너뛰죠. 테스트가 임시로 망가졌거나, 다른 걸 작업하는 동안 잠시 무시하고 싶을 때 편리해요:

test.skip('not ready yet', () => {
  // this test is skipped
})

.todo 는 아직 작성하지 않은 테스트의 자리표시자를 남겨줘요. 출력에 나열되기 때문에 잊어버릴 일이 없죠:

test.todo('implement validation later')

이런 수식어는 개발 중 빠르게 임시로 쓸 때 좋아요. 파일명·줄 번호·태그로 필터링하는 더 영구적인 방법은 Test Filtering 가이드를 보세요.

파라미터화된 테스트 (Parameterized Tests)

입력과 기대 출력만 다른 테스트 케이스가 여러 개라면, 케이스마다 test 를 하나씩 쓰는 건 반복이 심해요. test.for 는 케이스를 데이터로 정의하고, 모든 케이스에 같은 테스트 로직을 돌려줘요:

import { expect, test } from 'vitest'

test.for([
  [1, 1, 2],
  [1, 2, 3],
  [2, 1, 3],
])('add(%i, %i) -> %i', ([a, b, expected]) => {
  expect(a + b).toBe(expected)
})

위 예시의 %i 자리표시자는 각 데이터 행의 정수 값으로 바뀌어요. Vitest 는 %s(문자열), %f(부동소수점) 같은 자리표시자도 지원해요. 그래서 러너는 add(1, 1) -> 2, add(1, 2) -> 3, add(2, 1) -> 3 같은 테스트 이름을 만들어내요.

케이스에 값이 두세 개보다 많아지면 객체를 넘기는 쪽이 더 읽기 좋아요. 이름에 $property 를 쓰면 필드를 끼워 넣을 수 있어요:

test.for([
  { a: 1, b: 1, expected: 2 },
  { a: 1, b: 2, expected: 3 },
  { a: 2, b: 1, expected: 3 },
])('add($a, $b) -> $expected', ({ a, b, expected }) => {
  expect(a + b).toBe(expected)
})

테스트 함수의 두 번째 인자는 Test Context 인데, 픽스처·테스트별 expect·기타 유틸에 접근할 수 있어요. 특히 test.concurrent 와 함께 쓰일 때 유용한데, 동시 테스트는 병렬로 돌아가서 전역 expect 가 스냅샷을 올바른 테스트에 확실히 연결하지 못할 수 있거든요. 컨텍스트 스코프의 expect 가 이 문제를 풀어줘요:

test.concurrent.for([
  [1, 1],
  [1, 2],
  [2, 1],
])('add(%i, %i)', ([a, b], { expect }) => {
  expect(a + b).toMatchSnapshot()
})

describe.for 도 같은 방식으로 동작하되, 파라미터 묶음마다 스위트를 하나씩 만들어요. 여러 테스트가 같은 파라미터 셋업을 공유할 때 유용하죠.

::: tip Vitest 는 Jest 에서 익숙한 test.each 도 제공해요. 비슷하게 동작하지만 배열 인자를 단일 값으로 넘기는 대신 펼쳐서 넘기고, Test Context 에는 접근하지 못해요. 주로 Jest 호환용으로 존재하니, 새 코드에서는 test.for 를 쓰는 걸 권장해요. :::

전역 import 사용하기

기본적으로는 모든 테스트 파일 상단에서 test, expect, describe 같은 함수를 vitest 에서 import 해요. import 없이 전역으로 쓰고 싶다면(Jest 처럼) 설정에서 globals 옵션을 켜면 돼요:

import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    globals: true,
  },
})

이걸 켜면 import 줄 없이도 테스트를 작성할 수 있어요:

test('no import needed', () => {
  expect(1 + 1).toBe(2)
})

::: tip TypeScript 를 쓴다면 tsconfig.jsoncompilerOptions"types": ["vitest/globals"] 를 추가해야 타입 지원이 제대로 돼요. :::

테스트 실행하기

Vitest 는 기본적으로 모든 테스트 파일을 병렬로 실행해요(child processes 사용). 각 테스트 파일은 고립된 컨텍스트에서 돌기 때문에 파일끼리 상태를 공유하지 않아요. 그래서 서로 다른 파일의 테스트가 우연히 간섭하는 일을 막아주죠. 한 파일 안의 테스트는 기본적으로 순차 실행되는데, 같은 파일 안의 테스트는 셋업 코드를 공유하는 경우가 많아서 대개 이게 원하는 동작이에요. 정말 독립적이라면 test.concurrent 로 동시 실행에 들어가 속도를 높일 수 있고, 테스트 실행 제어에 대한 자세한 내용은 Parallelism 가이드를 참고하세요.

더 알아보기 (Learn more)