DEV/FE

[FE] 쉽게 이해하는 Vitest 주요 기능 완성 가이드

krokerdile 2024. 11. 17. 23:25
728x90

들어가며

네이버 부스트캠프에서 프론트엔드 테스트 코드 작성의 필요성을 느끼게 되었습니다. 그래서 이제는 리액트의 defacto가 된 Vite에서 사용하는 Vitest를 적용하기 위해 공부하면서 알게 된 내용들을 정리해보았습니다.

왜 테스트 코드가 필요했나요?

부스트캠프에서 프로젝트를 진행하면서 다음과 같은 상황들을 자주 마주쳤습니다:

  1. 🐛 예상치 못한 버그 발생
    • "잘 되던 기능이 갑자기 작동하지 않아요!"
    • "다른 팀원의 코드를 수정했더니 연관된 기능이 깨졌어요."
  2. 🤝 팀 협업의 어려움
    • 코드 리뷰할 때 해당 기능이 제대로 동작하는지 일일이 확인하기 힘들었습니다.
    • 다른 팀원의 코드를 이해하고 수정하는 데 시간이 많이 걸렸습니다.
  3. 🏃‍♂️ 빠른 개발 주기
    • 새로운 기능을 추가할 때마다 모든 기능을 수동으로 테스트하기에는 시간이 부족했습니다.
    • 배포 전 확인 과정이 너무 오래 걸렸습니다.

Vitest를 선택한 이유

  1. 🚀 Vite와의 완벽한 통합
    • Vite 기반 프로젝트에서 추가 설정 없이 바로 사용 가능
    • 빠른 HMR(Hot Module Replacement)로 개발 생산성 향상
  2. 💡 Jest와 유사한 API
    • Jest를 사용해본 개발자라면 쉽게 적응 가능
    • 풍부한 레퍼런스와 커뮤니티 지원
  3. 뛰어난 성능
    • 병렬 테스트 실행으로 빠른 테스트 속도
    • Watch 모드에서 변경된 파일만 테스트하여 효율적

이 가이드를 통해 얻을 수 있는 것

✅ Vitest의 기본 개념과 주요 기능 이해
✅ 실제 프로젝트에 바로 적용할 수 있는 실용적인 예제
✅ 테스트 코드 작성의 모범 사례와 팁
✅ 개발 생산성과 코드 품질 향상을 위한 노하우

자, 이제 Vitest의 주요 기능들을 하나씩 살펴보면서, 어떻게 하면 효과적으로 테스트 코드를 작성할 수 있는지 알아보겠습니다.

1. 테스트 구조화 함수들

🎓 쉬운 설명: 시험문제를 풀 때를 생각해봅시다. 큰 과목(수학)이 있고, 그 안에 중단원(도형), 소단원(삼각형)이 있는 것처럼 테스트도 비슷하게 구성합니다. 이렇게 구조를 잡으면 나중에 어떤 부분에서 문제가 있었는지 쉽게 찾을 수 있어요!

describe()

  • 연관된 테스트들을 논리적으로 그룹화하는 컨테이너 함수
  • 중첩 가능하여 계층적인 테스트 구조 생성 가능
describe('사용자 관리', () => {
  describe('회원가입', () => {
    // 회원가입 관련 테스트들
  });
  describe('로그인', () => {
    // 로그인 관련 테스트들
  });
});

it() / test()

  • 개별 테스트 케이스를 정의하는 함수
  • it()과 test()는 동일한 기능, BDD 스타일과 TDD 스타일의 차이
it('유효한 이메일로 회원가입이 성공한다', () => {
  // 테스트 로직
});

test('잘못된 비밀번호로 로그인 시 에러가 발생한다', () => {
  // 테스트 로직
});

🎓 TDD vs BDD 스타일 설명:

시험문제를 작성하는 두 가지 방식이라고 생각해보세요!

  • TDD(Test-Driven Development) 스타일: "이것을 테스트한다"라는 관점
  • test('비밀번호는 8자 이상이어야 한다', () => { // 테스트 로직 });
  • BDD(Behavior-Driven Development) 스타일: "이렇게 동작해야 한다"는 관점
  • it('비밀번호가 8자 미만이면 에러를 표시해야 한다', () => { // 테스트 로직 });

2. 검증 함수들

🎓 쉬운 설명: 선생님이 시험지를 채점할 때처럼, 우리가 예상한 답과 실제 결과가 맞는지 확인하는 부분입니다. expect()는 "이 답이..."라고 시작하는 문장이고, 뒤에 오는 matcher 함수들은 "...이것과 같아야 해"라고 끝나는 문장이라고 생각하면 됩니다!

expect()와 주요 matcher 함수들

// 기본적인 값 비교
expect(value).toBe(expected);          // "이 값이 정확히 이것과 같아야 해!"
expect(value).toEqual(expected);       // "이 값이 내용적으로 이것과 같아야 해!"
expect(value).toBeTruthy();           // "이 값이 참이어야 해!"
expect(value).toBeFalsy();            // "이 값이 거짓이어야 해!"

// 숫자 관련
expect(value).toBeGreaterThan(3);     // "이 값이 3보다 커야 해!"
expect(value).toBeLessThan(5);        // "이 값이 5보다 작아야 해!"
expect(value).toBeCloseTo(0.3);       // "이 값이 0.3과 비슷해야 해!"

// 객체/배열 관련
expect(object).toHaveProperty('key');  // "이 객체가 이런 속성을 가지고 있어야 해!"
expect(array).toContain(item);        // "이 배열이 이 항목을 포함해야 해!"
expect(object).toMatchObject({...});   // "이 객체가 이런 구조를 가져야 해!"

3. 설정 및 정리 함수들

🎓 쉬운 설명: 운동을 시작하기 전에 준비운동을 하고, 끝나고 나서 정리운동을 하는 것처럼, 테스트도 시작 전에 준비할 것들과 끝나고 나서 정리할 것들이 있습니다.

beforeEach() / afterEach()

  • 각각의 테스트 케이스 전/후에 실행되는 함수
beforeEach(() => {
  testDB = new TestDatabase();  // 각 테스트 전에 새로운 테스트 DB 준비
});

afterEach(() => {
  testDB.clear();  // 각 테스트 후에 DB 정리
});

beforeAll() / afterAll()

🎓 쉬운 설명: 체육대회를 시작하기 전에 운동장을 한 번만 준비하고, 끝나고 나서 한 번만 정리하는 것처럼 작동해요!

beforeAll(async () => {
  await initializeTestEnvironment();  // 모든 테스트 시작 전 한 번만 실행
});

afterAll(async () => {
  await cleanupTestEnvironment();     // 모든 테스트 완료 후 한 번만 실행
});

4. 목킹(Mocking) 기능

🎓 쉬운 설명: 영화 촬영할 때 위험한 장면은 스턴트맨이 대신하는 것처럼, 테스트할 때도 실제 시스템 대신 안전한 가짜를 사용할 수 있어요!

vi.fn()

  • 목 함수 생성
const mockFn = vi.fn();
mockFn.mockReturnValue('목 결과');
mockFn.mockImplementation(() => '구현된 목 결과');

vi.spyOn()

🎓 쉬운 설명: CCTV처럼 어떤 함수가 몇 번 불렸는지, 어떤 값을 받았는지 감시할 수 있어요!

const spy = vi.spyOn(object, 'method');
expect(spy).toHaveBeenCalled();        // "이 함수가 호출되었는지 확인"
expect(spy).toHaveBeenCalledTimes(2);  // "이 함수가 2번 호출되었는지 확인"

vi.mock()

🎓 쉬운 설명: 진짜 데이터베이스 대신 가짜 데이터베이스를 통째로 만들어서 사용하는 것과 같아요!

vi.mock('./database', () => ({
  query: vi.fn().mockResolvedValue([]),
  connect: vi.fn()
}));

5. 비동기 테스트 지원

🎓 쉬운 설명: 라면을 끓일 때 물이 끓을 때까지 기다리는 것처럼, 프로그램에서도 기다려야 하는 작업들이 있어요. 이런 기다리는 작업을 테스트하는 방법들이에요!

async/await 지원

it('비동기 데이터를 가져온다', async () => {
  const data = await fetchData();
  expect(data).toBeDefined();
});

resolves/rejects 매처

it('프로미스가 성공적으로 완료된다', () => {
  return expect(Promise.resolve('데이터')).resolves.toBe('데이터');
});

it('프로미스가 에러로 실패한다', () => {
  return expect(Promise.reject('에러')).rejects.toBe('에러');
});

6. 특수 테스트 기능

🎓 쉬운 설명: 다양한 상황을 효율적으로 테스트할 수 있는 특별한 도구들이에요!

test.each()

  • 여러 테스트 케이스를 테이블 형태로 실행
test.each([
  ['유효한 이메일', 'test@example.com', true],
  ['잘못된 이메일', 'invalid-email', false]
])('%s인 경우 검증 결과는 %s이다', (_, email, expected) => {
  expect(validateEmail(email)).toBe(expected);
});

test.skip() / test.only()

🎓 쉬운 설명: 시험문제 중에서 "이 문제는 나중에 풀래요" 하거나 "이 문제만 먼저 풀래요" 하는 것처럼 테스트를 선택적으로 실행할 수 있어요!

test.skip('이 테스트는 건너뛴다', () => {
  // 실행되지 않음
});

test.only('이 테스트만 실행한다', () => {
  // 단독 실행
});

7. 스냅샷 테스팅

🎓 쉬운 설명: 사진을 찍어서 나중에 비교하는 것처럼, 프로그램의 결과물을 저장해두고 나중에 변경사항이 있는지 확인할 수 있어요!

toMatchSnapshot()

it('컴포넌트가 올바르게 렌더링된다', () => {
  const component = render(<MyComponent />);
  expect(component).toMatchSnapshot();
});

toMatchInlineSnapshot()

🎓 쉬운 설명: 스냅샷을 별도 파일이 아닌 테스트 코드 안에 바로 저장해요. 마치 폴라로이드 사진처럼요!

it('객체 구조가 일치한다', () => {
  const user = createUser('John');
  expect(user).toMatchInlineSnapshot(`
    {
      "name": "John",
      "id": 1
    }
  `);
});

🌟 정리

  1. Vitest는 프로그램이 제대로 작동하는지 확인하는 종합 테스트 도구입니다.
  2. 테스트는 구조화된 방식으로 작성되어 관리하기 쉽습니다.
  3. 다양한 상황(동기/비동기)과 데이터 형식(숫자/문자/객체 등)을 테스트할 수 있습니다.
  4. 목킹을 통해 안전하고 빠른 테스트가 가능합니다.
  5. 스냅샷으로 UI 변경사항을 쉽게 추적할 수 있습니다.
728x90