테스트 라이브러리 API

지리·2025년 9월 14일

https://vitest.dev/api/

테스트 작성

test
테스트 할 함수를 받고 테스트를 수행하는 하나의 단위

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

test.skip
특정 테스트가 실행되는 것을 스킵

test.skip('skipped test', () => {
  // 스킵 되고 테스트 에러가 발생하지 않습니다.
  assert.equal(Math.sqrt(4), 3)
})

test.only
특정 테스트만 실행

test.only('test', () => {
  // 다른 테스트가 있어도 이 테스트만 실행됩니다.
  assert.equal(Math.sqrt(4), 2)
})

test.runIf
특정 환경에서만 테스트를 실행

const isDev = process.env.NODE_ENV === 'development'

test.runIf(isDev)('dev only test', () => {
  // 개발환경에서만 실행됩니다.
})

test.concurrent
테스트를 병렬로 실행

describe('suite', () => {
  test('serial test', async () => { /* ... */ })
  // concurrent test 1, 2가 동시에 실행됩니다.
  test.concurrent('concurrent test 1', async () => { /* ... */ })
  test.concurrent('concurrent test 2', async () => { /* ... */ })
})

test.sequential
테스트를 순차적으로 표시

describe.concurrent('suite', () => {
  test('concurrent test 1', async () => { /* ... */ })
  test('concurrent test 2', async () => { /* ... */ })

	// 병렬로 동작하지만, 아래 두개는 순차적으로 표시되는 것이 보장됩니다.
  test.sequential('sequential test 1', async () => { /* ... */ })
  test.sequential('sequential test 2', async () => { /* ... */ })
})

test.todo
테스트 구현은 아직 안 했지만 보고서에 표시하기 위해 사용

test.todo('테스트 구현 예정')

테스트 그룹화

describe
테스트를 하나의 그룹으로 모을 때 사용

describe('sort', () => {
  test('오름차순 정렬', () => {
    ...
  });
  
  bench('5개 요소', () => {
    ...
  });
});

describe.skip
특정 테스트 그룹이 테스트 실행되는 것을 스킵

describe.skip('실행되지 않는 테스트', () => {
  ...
})

describe.only
특정 테스트 그룹만 테스트 실행

describe.only('이것만 실행되는 테스트', () => {
  ...
});

테스트마다 실행해야 하는 훅

beforeEach
각각 테스트가 실행 되기 전에 호출되는 훅

beforeEach(async () => {
  // 예를 들어 모든 테스트에서 필요한 객체를 초기화 한다거나 공통 준비과정을 가집니다.
  await prepareModules();
});

afterEach
각각 테스트가 실행된 후에 호출되는 훅

afterEach(async () => {
  // 예를 들어 모든 테스트에서 필요한 객체를 해제 한다거나 공통 준비과정을 가집니다.
  await destroyModules()
});

beforeAll
모든 테스트가 실행되기 전에 호출되는 훅

beforeAll(async () => {
	// mocking 서버를 실행 합니다.
  await startMockingServer();
});

afterAll
모든 테스트가 실행된 후에 호출되는 훅

beforeAll(async () => {
	// mocking 서버를 정지합니다.
  await stopMockingServer();
});

Assertion 관련 API

Assertion이란 기대한 결과와 실제 결과가 같은지 확인하는 문장

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

test("2 + 3 = 5", () => {
  expect(add(2, 3)).toBe(5); // 여기가 assertion
});

Assertion 대상 생성

expect

기대값과 비교 하기위해 객체를 생성

expect(input).toBe(2); // expect로 생성한 후 기대값 '2'와 같은지 검증 합니다.

Assertion 수행하기

toBe

=== 비교

const count = 13;

test('count는 13입니다.', () => {
  expect(count).toBe(13)
})

not

const count = 11;

test('count는 13이 아닙니다', () => {
  expect(count).not.toBe(13)
})

toEqual

toBe 처럼 === 비교지만, 재귀적으로 깊은 비교를 진행

const stock = {
  type: 'apples',
  count: 13,
}

test('사과 재고가 13개 있습니다.', () => {
	// 이 검증은 성공합니다.
  expect(stock).toEqual({ type: 'apples', count: 13 });
	// 이 검증은 실패합니다.
  expect(stock).toBe({ type: 'apples', count: 13 });
});

toStrictEqual

기본적으로 toEqual과 동일하지만, 더 엄격하게 비교
대표적으로 undefined, constructor 비교

class Stock {
  constructor(type) {
    this.type = type;
  }
}

test('structurally the same, but semantically different', () => {
	// 성공 합니다.
  expect(new Stock('apples')).toEqual({ type: 'apples' });
	expect([,3]).toEqual([undefined, 3]);
  
  // 실패 합니다.
  expect(new Stock('apples')).toStrictEqual({ type: 'apples' });
	expect([,3]).toStrictEqual([undefined, 3]);  
});

toContain

‘요소’라고 볼 수 있는 대상이면 포함 여부를 검증 (배열, 문자열, DOM 요소)

test('toContain 써보기', () => {
  expect('my orange').toContain('orange');
  // 클래스 배열에서 문자열 확인하기
  expect(document.querySelector('#el').classList).toContain('flex');
  // 요소가 자식 요소인지 확인하기
  expect(document.querySelector('#wrapper')).toContain(document.querySelector('#el'));
});

toHaveLength

length 프로퍼티를 비교

test('toHaveLength', () => {
  expect([1, 2, 3]).toHaveLength(3);
  expect('abc').not.toHaveLength(3);
  expect({ length: 3 }).toHaveLength(3);
})

toHaveProperty

프로퍼티를 갖고 있는지 비교

const invoice = {
  isActive: true
};

test('John Doe Invoice', () => {
  expect(invoice).toHaveProperty('isActive');
});

toMatch

정규식을 이용해서 비교

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


#### toThrowError
에러가 던져지는지 확인
```js
function iThrowError() {
  throw new Error("i'm not error");
}

test('throws on pineapples', () => {
  // 정규식으로 에러 메세지를 검증할 수 있습니다.
  expect(() => iThrowError()).toThrowError(/not/)
})

toHaveBeenCalled

함수가 호출되었는지 확인

const iAmMock = {
  doing() {/* ... */},
};

test('mock function', () => {
	// mock에 대한 자세한 내용은 뒤에서 다룰 수 있습니다.
  const doing = vi.spyOn(iAmMock, 'doing');

	// doing은 호출되지 않았습니다.	
  expect(doing).not.toHaveBeenCalled();

  iAmMock.doing();

	// doing이 호출 되었습니다.
  expect(doing).toHaveBeenCalled()
})

toSatisfy

조건문을 통해 확인

const isOdd = (value: number) => value % 2 !== 0

it('1은 홀수입니다.', () => {
  expect(1).toSatisfy(isOdd);
});

스냅샷 비교하기

스냅샷 테스트는 어떻게 동작하나요?
toMatchSnapshot() 호출과 같은 특정 기준점에 스냅샷 파일(*.snap)로 저장합니다. 그리고 테스트할 새로운 스냅샷을 기존 스냅샷과 비교합니다. 이렇게 참조 기준점을 관리하기 쉽도록 설계되어 있습니다. 또한 u 키를 눌러 스냅샷을 쉽게 업데이트 할 수 있습니다.
RTL과 함께 사용할 경우 컴포넌트의 렌더링 결과(React Element 구조)를 스냅샷으로 저장합니다. (App.test.tsx.snap)

toMatchSnapshot

스냅샷을 비교

test('스냅샷 테스트', () => {
  const data = { foo: new Set(['bar', 'snapshot']) }
  
  // - 만약 테스트를 실행할 때 스냅샷이 없다면 새로운 스냅샷 파일을 생성할 것입니다.
  // - 다음번에 테스트할 때 이전 스냅샷과 비교하여 다르면 에러를 던집니다.
  expect(data).toMatchSnapshot();
});

toMatchInlineSnashot

파일이 아니라 인라인 문자열을 사용

test('인라인 스냅샷 테스트', () => {
  const data = { foo: new Set(['bar', 'snapshot']) }

  // 인라인 문자열을 이용해서 스냅샷 테스트를 수행합니다.
  expect(data).toMatchInlineSnapshot(`
    {
      "foo": Set {
        "bar",
        "snapshot",
      },
    }
  `)
})

toMatchFileSnapshot

snap 파일 대신 다른 파일을 사용

it('파일 스냅샷 테스트', async () => {
  const result = renderHTML(h('div', { class: 'foo' }));
  
  await expect(result).toMatchFileSnapshot('./test/basic.output.html')
})

RTL 기본개념 살펴보기

렌더링 -> 사용자 인터랙션 -> 결과 검증할 대상(UI)가져오기 -> 검증하기

렌더링 하기

실제 브라우저 엔진을 이용해서 시뮬레이션 하면 테스트 비용이 매우 크고 다양한 변수가 존재할수 있음. 그래서 RTL은 리액트 렌더링을 통한 가상 DOM 객체 이용

render

렌더링 수행

import {render} from '@testing-library/react';

test('Login', () => {
  render(<Login />)
})

screen

렌더링 된 화면처럼 인터페이스가 구현된 객체에 접근

import {render, screen} from '@testing-library/react';

test('should show login form', () => {
  render(<Login />)
  
  // 화면에서 Username 레이블 텍스트를 가진 요소를 가져옵니다.
  const input = screen.getByLabelText('Username');
})

대상 가져오기

고려사항
1. 사용자 인터렉션 혹은 렌더링은 비동기로 동작하는 경우가 많다. 따라서 렌더링이 모두 되었는지 몇번 재시도 하거나 기다리는 동작이 필요할 수 있다.
2. ‘요소를 가져온다’는 동작이 ‘Given’일 수도 있지만, ‘Assertion’일 수도 있다.

  • 단일 요소 가져오기

    쿼리 유형0개 일치1개 일치1개 이상 일치재시도(비동기)
    getBy...오류 발생요소 반환오류 발생아니요
    queryBy...null 반환요소 반환오류 발생아니요
    findBy...오류 발생요소 반환오류 발생
  • 다중 요소 가져오기

    쿼리 유형0개 일치1개 일치1개 이상 일치재시도(비동기)
    getAllBy...오류 발생배열 반환배열 반환아니요
    queryAllBy...[] 반환배열 반환배열 반환아니요
    findAllBy...오류 발생배열 반환배열 반환

getByRole

접근성 트리에서 role을 기반으로 가져옴

test('', () => {
	// role 목록: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques#roles
	screen.getByRole('button');
});

getByTestId

test용 id 속성을 넣어 가져옴

test('', () => {
	// data-testid="test-button" 속성을 가진 요소를 가져옵니다.
	screen.getByTestId('test-button');
});

getByText

텍스트를 기반으로 가져옴

// <div>Hello World</div>

screen.getByText('Hello World') // 텍스트가 매치되는 요소를 가져옴
screen.getByText('llo Worl', {exact: false}) // 일부만 매치되도 가져옴
screen.getByText(/World/) // 정규식 활용해서 가져옴
screen.getByText((content, element) => content.startsWith('Hello')) // 함수로 가져옴

Manual Queries

dom selector 를 이용해 직접 가져옴

const {container} = render(<MyComponent />)
const foo = container.querySelector('[data-foo="bar"]')

검증하기

검증할때는 위에서 확인했던 Assertion API를 그대로 활용

기타 비동기 유틸

비동기적으로 인터렉션이나 요소의 생성/수정/삭제 등을 기다려야 하는 상황에 유틸 사용가능.

waitFor

일정 시간마다 콜백 함수를 호출해서 완료될 때 까지 기다림

await waitFor(() => expect(mockAPI).toHaveBeenCalledTimes(1))

waitForElementToBeRemoved

요소가 사라질때까지 기다림

await waitForElementToBeRemoved(() => getByText(/not here/i))

사용자 인터렉션 관련 API

1. fireEvent
DOM 이벤트를 발생시키는 메서드로, 다양한 이벤트 유형에 대한 편의 메서드를 제공.
사용자가 생성한 액션(이벤트)을 테스트하기 위해 fireEvent API를 사용.
클릭 이벤트나 입력 이벤트 등을 DOM에 직접 dispatch 하여 시뮬레이션

주의사항
사용자가 요소를 클릭할 때 클릭 이벤트만 발생하는 것은 아닙니다. mouseOver → mouseMove → mouseDown → focus → mouseUp → click 과 같이 여러 이벤트가 순차적으로 발생합니다. 하지만 fireEvent는 해당 이벤트만 발생시키기 때문에 실제 사용자 관점의 테스트가 시뮬레이션 되기 어려울 수 있습니다.

2. userEvent
fireEvent를 기반으로 만들어진 라이브러리로, 사용자가 실제로 수행할 수 있는 상호작용을 시뮬레이션.
라벨을 클릭하는 것과 같은 요소의 유형에 기반한 상호작용을 시뮬레이션

사용자의 동작과 유사하게 테스트하는것이 테스트 신뢰도를 높이기때문에 userEvent 사용을 권장. 대부분의 테스트는 userEvent로 작성이 가능하며, userEvent에서 제공되지 않는 사례를 테스트할경우 fireEvent를 사용하는게 좋음.

DOM 이벤트 트리거하기

fireEvent

직접 요소와 이벤트 객체를 인자로 줘서 dispatch

fireEvent(
  getByText(container, 'Submit'),
  new MouseEvent('click', {
    bubbles: true,
    cancelable: true,
  }),
);

// 이런 방식도 가능합니다.
// fireEvent.click(getByText(container, 'Submit'));

createEvent

이벤트 객체 생성을 돕는 유틸

createEvent('input', inputNode, {
  target: {files: inputFiles}, // file을 input 트리거 시킬 수 있습니다.
  ...init,
})

// 이렇게도 됩니다.
// createEvent.input(inputNode, { ... })

사용자 이벤트 트리거

userEvent.setup

userEvent의 세션을 시작

const user = userEvent.setup()
await user.keyboard('[ShiftLeft>]')

userEvent

fireEvent와 마찬가지로 user[eventName]으로 트리거

user.click()
user.keyboard()
user.pointer()

pointer

마우스와 터치와 같이 포인터 기반 기기를 시뮬레이션

pointer({target: element, offset: 2, keys: '[MouseLeft]'})

keyboard

키보드 입력을 시뮬레이션

keyboard('foo')
keyboard('{Shift}{f}{o}{o}') // Shift, f, o, o 가 입력됩니다
keyboard('{{a[[') // {, a, [가 입력됩니다. 예약 문자는 두번 입력하면 입력 됩니다.
keyboard('{\\}}') // }가 입력 됩니다. 백슬래쉬로 escape 처리할 수 있습니다.
keyboard('[ShiftLeft][KeyF][KeyO][KeyO]') // Shfit, f, o, o가 입력됩니다. []로 keyCode를 통해 입력할 수 있습니다.

copy

클립보드로 복사

copy()

cut

클립보드로 오려두기

cut()

paste

클립보드에 있는 문자를 붙여넣기

past(clipboardData?)

Moking 관련 API

실행을 추적할 함수 만들기

fn

실행을 추적할 수 있는 mock 함수로 만듦

const fn = vi.fn()

fn('hello world');
fn.mock.calls[0] === ['hello world']

spyOn

객체의 메서드 또는 함수 프로퍼티를 실행을 추적할 수 있는 mock 함수로 만듦

const market = {
  getApples: () => 100
}

const getApplesSpy = vi.spyOn(market, 'getApples')
market.getApples()
getApplesSpy.mock.calls.length === 1

모킹 구현하기

fn().mockImplementation

mock 함수에 구현을 추가

const mockFn = vi.fn().mockImplementation(apples => apples + 1);
// fn에서 바로 인자로 줘서 가능합니다: vi.fn(apples => apples + 1);

const NelliesBucket = mockFn(0);
const BobsBucket = mockFn(1);

NelliesBucket === 1 // true
BobsBucket === 2 // true

mockFn.mock.calls[0][0] === 0 // true
mockFn.mock.calls[1][0] === 1 // true

fn().mockImplementationOnce

mockImplem 한번만 호출

const myMockFn = vi
  .fn(() => 'default')
  .mockImplementationOnce(() => 'first call')
  .mockImplementationOnce(() => 'second call');

// 'first call', 'second call', 'default', 'default'
console.log(myMockFn(), myMockFn(), myMockFn(), myMockFn());

fn().mockResolvedValue

특정 값을 비동기적으로 resolve 하는 함수를 추가

const asyncMock = vi.fn().mockResolvedValue(42);
await asyncMock(); // 42

fn().mockRejectedValue

특정 값을 비동기적으로 reject 하는 함수를 추가

const asyncMock = vi.fn().mockRejectedValue(new Error('Async error'));
await asyncMock(); // throws "Async error"

fn().mockReturnValue

특정 값을 리턴하는 함수를 추가

const mock = vi.fn();
mock.mockReturnValue(42);
mock(); // 42
mock.mockReturnValue(43);
mock(); // 43

fn().mockReturnThis

this 값을 리턴하는 함수를 추가

// 아래와 동일합니다.
spy.mockImplementation(function () {
  return this
})

실행내역 확인하기

mock.calls

실행할 때 언떤 인자가 들어왔는지 실행 내역을 확인가능

const fn = vi.fn();

fn('arg1', 'arg2');
fn('arg3');

console.log(fn.mock.calls)
/*
[
  ['arg1', 'arg2'], // first call
  ['arg3'], // second call
];
*/

mock.lastCall

가장 최근의 실행 내역을 가져옴. 없으면 undefined

const fn = vi.fn();

fn('arg1', 'arg2');
fn('arg3');

console.log(fn.mock.lastCall)
/*
['arg3'];
*/

mock.results

실행 결과에 대해 가져옴

const fn = vi.fn()
  .mockReturnValueOnce('result')
  .mockImplementationOnce(() => { throw new Error('thrown error') })

try { fn() } catch {}

console.log(fn.mock.results)
/*
[
  {
    type: 'return',
    value: 'result',
  },
  {
    type: 'throw',
    value: Error,
  },
]
*/

API Moking 관련 API

https://mswjs.io/docs/integrations/browser

npm i -D msw
// vite.comfig.js
{
	...
	test: {
    globals: true,
    environment: 'jsdom',
    setupFiles: ['vitest.setup.js'],
  },
}
// vite.setup.js
import { beforeAll, afterEach, afterAll } from 'vitest';
import { serviceWorker } from './mock/workers';

beforeAll(() => {
  console.log('server start');
  serviceWorker.listen();
});

afterEach(() => {
  serviceWorker.resetHandlers();
});

afterAll(() => {
  serviceWorker.close();
});
// mock/worker.js
import { setupServer } from "msw/node";
import { handlers } from "./handlers";

export const serviceWorker = setupServer(...handlers);
// mock/handler.js
import { http, HttpResponse } from "msw";

export const handlers = [
  http.get("http://localhost:8000/api/greet", async () => {
    await new Promise(resolve => setTimeout(resolve, 300));
    
    return HttpResponse.json({
      message: 'Hello World',
    })
  }),
];
profile
공부한것들, 경험한 것들을 기록하려 노력합니다✨

0개의 댓글