🚀 94sssh
Published on

2023.12.12

Vitest 알아보기

테스팅 라이브러리

Vitest는 테스팅 라이브러리입니다.
테스트 코드 작성을 도와주는 도구로, 이를 통해 테스트 자동화를 구성할 수 있고 실행 결과가 기대와 일치하는지 확인할 수 있습니다.

테스트 코드를 작성하면 다음과 같은 장점을 얻을 수 있습니다.

  1. 코드를 작성하기 전에 코드에 대해 생각하게 됩니다.
    단위 테스트를 작성해야 하므로, 기능에 대한 충분한 이해가 필요하고 이를 통해 명확한 기능에 대한 코드를 작성할 수 있습니다. 또한, 테스트를 위해 코드를 분리하면서 간결한 코드를 작성할 수 있습니다.

  2. 버그를 예방할 수 있습니다.
    코드를 수정할 때 예상치 못한 부작용을 사전에 방지할 수 있고, 각 부분이 원하는 대로 작동하는지 확인할 수 있어 버그를 식별하고 예방할 수 있습니다.

  3. 유지보수가 용이해집니다.
    대부분의 기능에 테스트를 작성하므로 최종적으로 문제가 있는 곳을 금방 파악할 수 있고, 기능을 추가하거나 변경할 때 기존 코드가 영향을 받는지 확인할 수 있습니다. 마찬가지로 리팩터링 과정에서 안정성을 유지할 수 있게 됩니다.

Jest 이전에는 Test Runner, Assertion Library, Test Mock 라이브러리 등을 조합하여 테스트 코드를 작성했습니다. 각각의 역할과 대표적인 라이브러리는 다음과 같습니다.

Mocha
  • Test Runner:
    테스트 코드를 실행하는 주체로, 테스트 케이스를 찾고 실행하는 역할을 담당합니다. 각 테스트 케이스를 식별하며, 실행 및 결과를 나타내는 등의 작업을 수행합니다.
    대표적으로 Mocha가 있습니다.

    Chai
  • Assertion Library: 테스트 코드에서 예상 결과와 실제 결과를 비교하고, 테스트를 더 명확하게 작성하는 데 도움이 되는 다양한 어설션(assertion) 함수를 제공합니다. 테스트 실행 시 실제 결과와 비교하여 코드의 정확성을 확인할 수 있습니다.
    대표적으로 Chai가 있습니다.

    Sinon
  • Test Mock
    테스트를 위한 모의 객체나 모듈을 생성하고, 특정 메서드가 호출되었는지, 얼마나 호출되었는지를 추적하거나 반환값을 설정하거나, 특정 조건이나 상황에서 어떻게 동작할지를 시뮬레이션하는 기능을 제공합니다.
    대표적으로 Sinon이 있습니다.


Jest 이후 Jest의 등장으로, 대부분의 기능을 담은 Jest가 대표적인 테스팅 라이브러리가 되었습니다.
JestNpmTrends
(적절한 비교가 아닐 수도 있지만...)

그렇지만 이번에 사용할 테스팅 라이브러리는 Jest가 아닌 Vitest입니다.
현재 프로젝트는 Vite를 사용 중인데 굉장히 만족스럽게 사용하고 있습니다.
이러한 Vite에 최적화되어 있는 테스팅 프레임워크이기도 하고, ESM을 사용하여 모듈을 로드하고 실행해 보다 빠른 속도와 성능 최적화를 지원합니다.
아직 v1.0.4로 v1.0.0이 릴리즈된 지 정확히 일주일밖에 되지 않았지만 기존 Jest의 장점을 따르면서 Vitest만의 매력이 있다고 생각해서 사용해 볼 생각입니다.

테스트 주도 개발에 대해 '테스트를 위한 테스트가 되어간다.', '테스트 코드 작성에 시간이 많이 쓰인다.'는 말을 듣고 테스트가 필요하지 않겠다는 생각을 했었습니다.
지금은 생각을 바꿔 테스트의 필요성과 중요성을 느끼고 Vitest를 도입하고자 내용을 정리해 보려고 합니다.

Vitest

설치

npm 혹은 yarn 등을 사용하여 설치합니다.

npm install -D vitest
yarn add -D vitest

Vitest 1.0에는 Vite >=v5.0.0 및 Node >=v18.00이 필요합니다.

세팅

Vitest를 실행하기 위해 package.json에 scripts를 정의합니다. 이때, vitest로 실행 모드에 진입하면 watch 모드로 실행되어 변경 사항이 생길때마다 테스트를 재실행하고 결과를 반환합니다.

package.json
{
  "scripts": {
    "test": "vitest"
  }
}

테스트를 한 번만 실행하도록 스크립트를 수정하려면 vitest run으로 정의합니다.

package.json
{
  "scripts": {
    "test": "vitest run"
  }
}

npm 혹은 yarn 등을 사용하여 테스트를 실행할 수 있습니다.

npm run test
yarn test

명령어를 통해서 실행할 수도 있습니다.
현재 경로를 기준으로 실행되며, vitest명령어의 인자로 경로를 전달할 수 있습니다. 전달된 경로에 포함된 테스트 파일만 실행됩니다.
마찬가지로 vitest run을 통해 한 번만 실행할 수 있습니다.

vitest foobar

테스트 코드 작성

아래와 같이 테스트 코드를 작성하고 실행해 볼 수 있습니다.
sum.test.js의 코드를 간략하게 살펴보겠습니다.
test는 테스트 함수로, 하나의 테스트 케이스를 정의합니다. 테스트 케이스의 이름과 로직을 포함합니다.
test함수의 내부 첫 번째 문자열 매개변수는 해당 테스트 케이스의 이름을 정의합니다.
test함수의 두 번째 매개변수로 전달된 함수는 실제 테스트 로직을 포함하고 있습니다. 테스트 코드가 어떻게 동작할지를 정의합니다.
expect함수는 어설션을 만드는데 사용됩니다. 여기서는 어설션 메서드 중 하나인 toBe와 함께 사용되었는데, toBe는 실행한 결과를 기댓값과 비교하여 동일한지 확인합니다.

sum.js
export function sum(a, b) {
  return a + b;
}
sum.test.js
import { expect, test } from 'vitest'
import { sum } from './sum'

test('adds 1 + 2 to equal 3', () => {
  expect(sum(1, 2)).toBe(3);
})
testresult

Vitest 구성

Vitest는 기본적으로 Vite와 통합된 구성을 사용해 vite.config.ts파일을 읽어 플러그인 등 설정을 Vite와 일치시킵니다.
Vitest를 위한 다른 구성을 원한다면 아래와 같이 구성할 수 있습니다.

  1. vitest.config.ts파일을 생성해 더 높은 우선순위를 갖도록 합니다.
  2. CLI를 통해 --config옵션을 전달하여 특정 구성 파일을 지정합니다.
    vitest --config ./path/to/vitest.config.ts
  3. vite.config.ts파일에서 process.env.VITEST 또는 defineConfigmode 속성을 사용해 테스트 중에만 다른 구성이 적용되도록 조건부로 구성을 적용할 수 있습니다.

Vitest은 Vite와 동일한 확장자를 지원하며, 구성 파일로 .js, .mjs, .cjs, .ts, .cts, .mts를 사용할 수 있습니다. .json은 지원하지 않습니다.

만약 Vite를 빌드툴로 사용하지 않는다면, vitest.config.ts파일에 test속성을 사용해 Vitest를 구성할 수 있습니다.

vitest.config.ts
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    // ...
  },
});

이미 Vite를 사용하고 있다면, vite.config.ts파일에 test 속성을 추가할 수 있습니다. 이때, 파일 상단에 트리플 슬래시 지시어를 사용하여 Vitest 타입 참조를 추가해야 합니다.

vite.config.ts
/// <reference types="vitest" />
import { defineConfig } from 'vite';

export default defineConfig({
  test: {
    // ...
  },
});

Vitest에서는 Vite와 Vitest 각각의 .config.ts파일을 만드는 것을 추천하지 않습니다. vitest.config.ts파일을 만들게 되면 vite.config.ts파일의 설정이 확장되는 것이 아니라 재정의를 하기 때문에 vitest.config.ts에서 동일한 옵션을 정의해 주어야 합니다. 그렇기에 mergeConfig메서드를 제공하여 두 구성을 병합할 수 있도록 하고 있습니다.

import { defineConfig, mergeConfig } from 'vitest/config'
import viteConfig from './vite.config.mjs'

export default mergeConfig(
  viteConfig,
  defineConfig({
    test: {
      // ...
    },
  })
)

테스트에 사용되는 Api

이제 테스트에 사용되는 Api들을 알아보겠습니다.

test

test는 테스트 코드를 정의합니다. 테스트의 이름과 기댓값을 포함하는 함수를 전달받습니다.
선택적으로 timeout을 전달해 테스트가 종료되기 전에 대기하는 시간을 전달할 수 있으며, 기본값은 5초입니다.
전역적으로 testTimeout을 구성할 수 있습니다.

  • Type: (name: string | Function, fn: TestFunction, timeout?: number | TestOptions) => void
import { expect, test } from 'vitest'

test('should work as expected', () => {
  expect(Math.sqrt(4)).toBe(2)
})

expect

expect는 어설션(assertion)을 생성하는데 사용됩니다. 이 컨텍스트에서 어설션은 명제를 단언하는 데 호출할 수 있는 함수를 의미합니다. Vitest는 Chai 어설션을 기본적으로 제공하며, Chai를 기반으로 한 Jest 호환 어설션도 제공합니다.

예를 들면, 다음 코드는 value 값이 5와 같아야 한다고 단언(assert)합니다. 만약 그렇지 않다면 어설션이 에러를 발생시키고 테스트가 실패합니다.

import { expect } from 'vitest'

const value = 5

test('값이 5와 같아야 함', () => {
  expect(value).toBe(5)
})

soft

expect.softexpect와 유사하게 동작하지만, 실패한 어설션에 대해 테스트 실행을 중지시키지 않고 계속 실행하며 해당 테스트를 실패로 표시합니다. 테스트 중에 발생한 모든 에러는 테스트가 완료될 때까지 표시됩니다.

import { expect, test } from 'vitest'

test('expect.soft test', () => {
  expect.soft(1 + 1).toBe(3) // 테스트를 실패로 표시하고 계속 진행
  expect.soft(1 + 2).toBe(4) // 테스트를 실패로 표시하고 계속 진행
})
// 테스트가 끝나면, 위의 에러들이 출력됩니다.

expect.softexpect와 함께 사용할 수도 있습니다. 만약 expect 어설션이 실패하면 테스트가 종료되고 모든 에러가 표시됩니다.
expect.softtest내에서만 사용할 수 있습니다.

import { expect, test } from 'vitest'

test('expect.soft test', () => {
  expect.soft(1 + 1).toBe(3) // 테스트를 실패로 표시하고 계속 진행
  expect(1 + 2).toBe(4) // 실패하고 테스트를 종료하며 이전의 모든 에러가 출력됨
  expect.soft(1 + 3).toBe(5) // 실행되지 않음
})

not

not을 사용하면 어설션을 부정할 수 있습니다. 예를 들어, 다음 코드는 value 값이 10과 같지 않아야 한다고 단언합니다. 만약 결괏값과 기댓값이 같다면 어설션이 에러를 발생시키고 테스트가 실패합니다.

import { expect, test } from 'vitest'

const value = 8

test('값이 10과 같지 않아야 함', () => {
  expect(value).not.toBe(10)
})

toBe

toBe는 원시 자료형(primitives)이 동일한지 혹은 객체들이 동일한 참조를 가지는지 테스트하는 데 사용됩니다. 만약 객체가 동일하지 않지만, 객체의 구조가 동일한지를 확인하려면 toEqual을 사용할 수 있습니다.

toBe를 부동 소수점 숫자와 함께 사용하지 않도록 합니다. 자바스크립트 숫자를 반올림하므로 0.1 + 0.2는 엄격하게 0.3이 아닙니다. 부동 소수점 숫자를 테스트에 사용하려면 toBeCloseTo 어설션을 사용하세요.

expect에 전달된 result의 값이 toBe에 전달된 기대값(5)과 동일한지 확인합니다. 결과가 5가 아니면 테스트가 실패합니다.

import { expect, test } from 'vitest'

function add(a, b) {
  return a + b
}

test('add function should correctly add two numbers', () => {
  const result = add(2, 3)
  expect(result).toBe(5)
})

toBeCloseTo

toBeCloseTo는 부동 소수점 숫자를 비교하는 데 사용됩니다. 선택적으로 numDigits 인자를 사용하여 소수점 자릿수에 제한을 둘 수 있습니다.

import { expect, test } from 'vitest'

test.fails('decimals are not equal in javascript', () => {
  expect(0.2 + 0.1).toBe(0.3) // 0.2 + 0.1 is 0.30000000000000004
})

test('decimals are rounded to 5 after the point', () => {
  // 0.2 + 0.1 is 0.30000 | "000000000004" removed
  expect(0.2 + 0.1).toBeCloseTo(0.3, 5)
  // nothing from 0.30000000000000004 is removed
  expect(0.2 + 0.1).not.toBeCloseTo(0.3, 50)
})

toBeDefined / toBeUndefined

toBeDefined는 값이 undefined와 다른지를 확인합니다.
toBeUndefinedundefined와 같은지 확인합니다.
함수가 반환하는 값이 있는지 여부를 확인하는 데 유용합니다.

import { expect, test } from 'vitest'

function getStock(name) {
  if (name === 'APPLE') {
    return { name: 'APPLE', price: 200 }
  }
}

test('getStock returned something', () => {
  const stock = getStock('APPLE')
  expect(stock).toBeDefined()
})

test('getStock returns undefined', () => {
  const stock = getStock('GOOGLE')
  expect(stock).toBeUndefined()
})

toBeTruthy / toBeFalsy

toBeTruthytoBeFalsy는 값이 boolean으로 변환됐을 때 true/false인지 확인합니다. 값 자체는 중요하지 않고 단순히 true/false로 변환될 수 있는지를 알고 싶을 때 유용합니다.

JavaScript에서 false, null, undefined, NaN, 0, -0, 0n, ""document.all을 제외하고는 모두 truthy입니다.

예를 들어, 아래 코드에서는 hasData의 반환 값에 신경 쓰지 않습니다. 이 값은 복잡한 객체, 문자열 또는 기타 등등이 될 수 있지만, 테스트는 반환 값이 true/false로 변환되었는지를 확인하고 진행합니다.

import { expect, test } from 'vitest'
import { hasData } from './hasData.ts'

test('hasData should return truthy for non-empty data', () => {
  // 실제 반환 값 자체보다는 반환 값이 truthy한 값인지, falsy한 값인지에 따라 진행
  expect(hasData([1, 2, 3])).toBeTruthy()
  expect(hasData([])).toBeFalsy()
})

toBeNull

toBeNull은 무언가가 null인지 확인합니다. .toBe(null)의 별칭입니다.

import { expect, test } from 'vitest'

function apples() {
  return null
}

test("we don't have apples", () => {
  expect(apples()).toBeNull()
})

toBeNaN

toBeNaN은 무언가가 NaN인지 확인합니다. .toBe(NaN)의 별칭입니다.

import { expect, test } from 'vitest'

let i = 0

function getApplesCount() {
  i++
  return i > 1 ? Number.NaN : i
}

test('getApplesCount has some unusual side effects...', () => {
  expect(getApplesCount()).not.toBeNaN()
  expect(getApplesCount()).toBeNaN()
})

toBeTypeOf

toBeTypeOf는 실제 값이 받은 타입과 동일한 타입인지 확인합니다.

import { expect, test } from 'vitest'

const price = 10000

test('price is type of number', () => {
  expect(actual).toBeTypeOf('number')
})

toBeGreaterThan / toBeGreaterThanOrEqual / toBeLessThan / toBeLessThanOrEqual

각각의 함수들은 이름에서 추론할 수 있듯이 반환값과 주어진 값을 비교하여 테스트를 실행합니다.

toBeGreaterThan: 주어진 값보다 큰지 확인
toBeGreaterThanOrEqual: 주어진 값보다 크거나 같은지 확인
toBeLessThan: 주어진 값보다 작은지 확인
toBeLessThanOrEqual: 주어진 값보다 작거나 같은지 확인

import { expect, test } from 'vitest'
import { getApples } from './stocks.js'

test('have more then 10 apples', () => {
  expect(getApples()).toBeGreaterThan(10)
})

test('have 11 apples or more', () => {
  expect(getApples()).toBeGreaterThanOrEqual(11)
})

test('have less then 20 apples', () => {
  expect(getApples()).toBeLessThan(20)
})

test('have 11 apples or less', () => {
  expect(getApples()).toBeLessThanOrEqual(11)
})

toEqual

toEqual은 실제 값이 받은 값과 동일하거나, 객체라면 동일한 구조를 가지는지 확인합니다(재귀적으로 비교). toEqualtoBe의 차이점은 다음 예제에서 확인할 수 있습니다:

import { expect, test } from 'vitest'

const personA = {
  name: 'John',
  age: 30,
}

const personB = {
  name: 'John',
  age: 30,
}

test('They have the same properties', () => {
  expect(personA).toEqual(personB)
})

test('They are not the same', () => {
  expect(personA).not.toBe(personB)
})

toContain

toContain은 실제 값이 배열에 있는지 확인합니다.
또한 toContain은 문자열이 다른 문자열의 부분 문자열인지 확인할 수 있습니다.

import { expect, test } from 'vitest'

test('Check if a value is included in an array', () => {
  const numbers = [1, 2, 3, 4, 5]

  expect(numbers).toContain(3)
})
import { expect, test } from 'vitest'

test('Check if a string is a substring of another string', () => {
  const sentence = 'check string'

  expect(sentence).toContain('check')
})

toHaveProperty

toHaveProperty은 주어진 키가 객체에 존재하는지 확인하는 어설션입니다.

또한 toEqual 매처와 같이 받은 속성 값과 비교할 수 있는 선택적인 값 인자를 제공할 수 있습니다.

import { expect, test } from 'vitest'

const dog = {
  name: 'Max',
  age: 5,
  feature: {
    species: 'Shepherd',
    from: 'Germany',
  },
}

test('Check properties of the dog object', () => {
  // 'name' 속성이 dog 객체에 존재하는지 확인
  expect(dog).toHaveProperty('name')

  // 'feature.species' 속성이 dog 객체에 존재하는지 확인
  expect(dog).toHaveProperty('feature.species')

  // 'color' 속성이 dog 객체에 존재하지 않는지 확인
  expect(dog).not.toHaveProperty('color')

  // 'feature' 안의 'from' 속성이 특정 값과 일치하는지 확인
  expect(dog).toHaveProperty('feature.from', 'Germany')
})

toMatch

toMatch는 문자열이 정규 표현식 또는 문자열과 일치하는지 확인합니다.

import { expect, test } from 'vitest'

test('top fruits', () => {
  expect('top fruits include apple, orange and grape').toMatch(/apple/)
  expect('applefruits').toMatch('fruit')
})

toMatchObject

toMatchObject는 객체가 다른 객체의 일부 속성과 일치하는지 확인합니다.

import { expect, test } from 'vitest'

const johnInvoice = {
  isActive: true,
  customer: {
    first_name: 'John',
    last_name: 'Doe',
  },
  items: [
    {
      type: 'apples',
      quantity: 10,
    },
    {
      type: 'oranges',
      quantity: 5,
    },
  ],
}

const johnDetails = {
  customer: {
    first_name: 'John',
    last_name: 'Doe',
  },
}

test('invoice has john personal details', () => {
  expect(johnInvoice).toMatchObject(johnDetails)
})

또한, 객체를 담은 배열을 전달할 수도 있습니다. 이는 두 배열의 요소의 수가 동일한지 확인할 때 유용합니다.

import { expect, test } from 'vitest'

test('the number of elements must match exactly', () => {
  const expectedArray = [
    { id: 1, name: 'Apple' },
    { id: 2, name: 'Orange' },
    { id: 3, name: 'Grape' },
  ]

  const actualArray = [
    { id: 1, name: 'Apple' },
    { id: 2, name: 'Orange' },
    { id: 3, name: 'Grape' },
  ]

  expect(actualArray).toMatchObject(expectedArray)
})

toThrowError

toThrowError는 함수가 호출될 때 에러를 발생시키는지 확인합니다.
선택적 인수를 제공해 특정한 오류가 발생하는지 테스트할 수 있습니다.

에러 메시지를 확인하기 위해 정규 표현식이나 문자열을 제공할 수 있습니다.

이때, 코드를 함수로 래핑하지 않으면 에러가 잡히지 않고 테스트가 실패합니다.

function getFruitStock(type) {
  if (type === 'pineapples') throw new Error('Pineapples are not in stock')

  // ...
}

test('throws on pineapples', () => {
  // 에러 메시지에 "stock"이라는 단어가 포함되어 있는지 테스트
  // 다음의 두 테스트 케이스는 동일합니다.

  expect(() => getFruitStock('pineapples')).toThrowError(/stock/)
  expect(() => getFruitStock('pineapples')).toThrowError('stock')

  // 정확한 에러 메시지를 테스트
  expect(() => getFruitStock('pineapples')).toThrowError(/^Pineapples are not in stock$/)
})

비동기 함수를 테스트하려면 rejects와 함께 사용하세요.

function getAsyncFruitStock() {
  return Promise.reject(new Error('empty'))
}

test('throws on pineapples', async () => {
  await expect(() => getAsyncFruitStock()).rejects.toThrowError('empty')
})

참고 문서 : https://vitest.dev/guide/