blog-imgDucklog

Vitest로 테스트 코드 작성하기!

Vitest로 테스트 코드 작성하기

왜 테스트 코드를 작성해야할까?

사실 테스트 코드를 작성하기 전에는, 테스트 코드의 필요를 크게 느끼지 못했습니다. 개발하면서 QA를 하면 모든 예외상황을 체크할 수 있을 것이라고 안일하게 생각했습니다. 또한 귀찮음촉박한 개발 일정이라는 이유로 외면했었습니다.


하지만 실제 프로덕션에 나간 코드가 예상치 못한 버그가 발생하고 코드에 대한 신뢰가 떨어지면서 테스트 코드의 필요성을 느끼게 되었습니다.


소프트웨어 개발 방법론 중 하나인 TDD(테스트 주도 개발)에는 우선적으로 테스트 코드를 작성하고 기능을 만들라고 되어있습니다.


실제로 A 기능에 대한 코드와 A 기능 테스트 코드를 작성하게 되면 2배로 코드를 작성하는 시간이 드는건 맞지만, 실제 테스트 시간, 나중에 버그로 인한 사이드 이펙트를 해결할 시간을 생각하면 어쩌면 시간을 더 절약할 수 있습니다.


또한 만약 다른 팀원이 작업한 코드를 복잡한 코드를 볼 때, 테스트 코드를 따라가다보면, 팀원이 작성한 코드의 기능을 파악하기 용이합니다. 이제 테스트 코드를 작성해야하는 이유를 알게 됐으니, 어떤 도구를 써야할까요?

Vitest vs Jest

  • 성능: Vitest는 Vite의 빠른 빌드 속도와 HMR 기능을 활용하여 Jest보다 빠른 테스트 실행 속도를 제공합니다.

  • 설정의 용이성: Vitest는 Vite와 자연스럽게 통합되어 설정이 간단하고 직관적입니다.

  • 호환성: Vitest는 Jest와 유사한 API를 제공하여 기존 Jest 사용자가 쉽게 전환할 수 있습니다.

왜 Vitest?

  • 빠른 개발 환경: Vite의 성능을 활용하여 매우 빠른 테스트 실행 속도를 제공합니다. 이는 개발자가 코드를 수정하고 테스트하는 과정에서 시간을 절약할 수 있게 합니다.

  • 핫 모듈 리플레이스먼트 (HMR): Vite의 HMR 기능을 통해 코드 변경 시 테스트 결과를 즉시 확인할 수 있어 개발 피드백 루프가 짧아집니다.

  • 간편한 설정: Vitest는 Vite 프로젝트와 자연스럽게 통합되어 설정이 간단하고 직관적입니다. 이는 설정 작업에 소요되는 시간을 줄이고, 더 많은 시간을 실제 개발에 집중할 수 있게 합니다.

  • 모던 프레임워크와의 호환성: Vitest는 TypeScript, JSX, Vue, React 등 최신 프레임워크 및 라이브러리와의 호환성이 뛰어나, 모던 웹 개발 환경에 적합합니다.

  • Jest와 유사한 API: Vitest는 Jest와 매우 유사한 API를 제공하여, 기존 Jest 사용자들이 쉽게 전환할 수 있습니다. 이는 학습 곡선을 낮추고, 빠르게 Vitest를 도입할 수 있게 합니다

vitest로 테스트 코드 작성하기

기본적인 테스트 작성법

import { describe, it, expect } from 'vitest';

describe('더하기 함수 테스트', () => {
  it('두 숫자를 덧셈합니다', () => {
    const sum = (a, b) => a + b;
    expect(sum(1, 2)).toBe(3);
  });
});

비동기 코드 테스트

import { describe, it, expect } from 'vitest';

describe('API 응답 테스트', () => {
  it('데이터를 받아온다', async () => {
    const fetchData = () => Promise.resolve('data');
    const data = await fetchData();
    expect(data).toBe('data');
  });
});

React 컴포넌트 테스트코드

// Counter.jsx
import React, { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p data-testid="count">{count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <button onClick={() => setCount(count - 1)}>Decrement</button>
    </div>
  );
}
export default Counter;
import { render, screen, fireEvent } from '@testing-library/react';
import { describe, it, expect } from 'vitest';
import Counter from './Counter';

describe('Counter Component', () => {
  it('초기 카운트를 렌더링한다', () => {
    render(<Counter />);
    const countElement = screen.getByTestId('count');
    expect(countElement.textContent).toBe('0');
  });

  it('카운트를 증가시킨다', () => {
    render(<Counter />);
    const countElement = screen.getByTestId('count');
    const incrementButton = screen.getByText('Increment');

    fireEvent.click(incrementButton);
    expect(countElement.textContent).toBe('1');
  });

  it('카운트를 감소시킨다', () => {
    render(<Counter />);
    const countElement = screen.getByTestId('count');
    const decrementButton = screen.getByText('Decrement');

    fireEvent.click(decrementButton);
    expect(countElement.textContent).toBe('-1');
  });
});

테스트 코드 작성 팁

재사용 가능한 테스트 유틸리티 함수

반복적인 테스트 코드를 줄이기 위해 유틸리티 함수를 작성합니다.

export const complexCalculation = (a, b) => {
  return (a * b) / (a - b);
};

describe('복잡한 계산', () => {
  it('정상 작동 테스트', () => {
    const result = complexCalculation(10, 5);
    expect(result).toBeCloseTo(2.5);
  });

  it('0으로 나누기를 처리합니다.', () => {
    const result = complexCalculation(5, 5);
    expect(result).toBeNaN();
  });
});

Mock을 활용한 테스트 코드

/**
 * 목 데이터 Fetch 함수
 * */
export const mockFetch = (url, options = {}) => {
  const { method = 'GET', response, status = 200, headers = {} } = options;
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({
        ok: true,
        status,
        json: async () => response,
        headers: new Headers(headers),
      });
    }, 100);
  });
};

global.fetch = vi.fn().mockImplementation(mockFetch);

describe('API 응답 테스트', () => {
  it('FETCH 성공', async () => {
    const mockResponse = { data: 'test' };
    fetch.mockImplementationOnce(() => mockFetch('/api/data', { response: mockResponse }));

    const response = await fetch('/api/data');
    const data = await response.json();

    expect(data).toEqual(mockResponse);
    expect(response.ok).toBe(true);
  });
});

Tanstack query 데이터 Fetch 테스트 코드

// DataComponent.jsx
import React from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import axios from 'axios';

const fetchData = async () => {
  const { data } = await axios.get('/api/data');
  return data;
};

const updateData = async (newData) => {
  const { data } = await axios.put('/api/data', newData);
  return data;
};

function DataComponent() {
  const queryClient = useQueryClient();

  const { data, error, isLoading } = useQuery(['data'], fetchData);

  const mutation = useMutation(updateData, {
    onSuccess: () => {
      queryClient.invalidateQueries(['data']);
    },
  });

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return (
    <div>
      <h1>Data</h1>
      <pre>{JSON.stringify(data, null, 2)}</pre>
      <button onClick={() => mutation.mutate({ newData: 'Updated data' })}>Update Data</button>
    </div>
  );
}

export default DataComponent;
// DataComponent.test.jsx
import { render, screen, waitFor, fireEvent } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { describe, it, expect, vi } from 'vitest';
import axios from 'axios';
import DataComponent from './DataComponent';

// axios Mock 설정
vi.mock('axios');

// mock 데이터
const mockData = { data: 'test data' };
const updatedData = { data: 'Updated data' };

const queryClient = new QueryClient();

describe('DataComponent', () => {
  it('데이터를 가져와서 화면에 표시한다', async () => {
    axios.get.mockResolvedValueOnce({ data: mockData });
    // mockResolvedValueOnce란 mock 함수가 한 번 호출될 때 지정된 값을 반환하도록 설정하는 메서드
    render(
      <QueryClientProvider client={queryClient}>
        <DataComponent />
      </QueryClientProvider>,
    );

    expect(screen.getByText('Loading...')).toBeInTheDocument();

    await waitFor(() => expect(screen.getByText(/test data/i)).toBeInTheDocument());
  });

  it('버튼 클릭 시 데이터를 업데이트한다', async () => {
    axios.get.mockResolvedValueOnce({ data: mockData });
    axios.put.mockResolvedValueOnce({ data: updatedData });

    render(
      <QueryClientProvider client={queryClient}>
        <DataComponent />
      </QueryClientProvider>,
    );

    await waitFor(() => expect(screen.getByText(/test data/i)).toBeInTheDocument());

    fireEvent.click(screen.getByText(/Update Data/i));

    await waitFor(() => expect(screen.getByText(/Updated data/i)).toBeInTheDocument());
  });
});

코드 설명

mock 설정

// axios Mock 설정
vi.mock('axios');

// mock 데이터
const mockData = { data: 'test data' };
const updatedData = { data: 'Updated data' };

const queryClient = new QueryClient();

vi.mock('axios'): axios 모듈을 모의(mock)하여 실제 네트워크 요청을 방지합니다.
mockData와 updatedData: 테스트에 사용할 모의 데이터입니다. 첫 번째는 초기 데이터, 두 번째는 업데이트된 데이터입니다.
queryClient: react-query를 사용하는 컴포넌트에 필요한 QueryClient 인스턴스를 생성합니다.

데이터 가져오기

it('데이터를 가져와서 화면에 표시한다', async () => {
  axios.get.mockResolvedValueOnce({ data: mockData });
  // mockResolvedValueOnce란 mock 함수가 한 번 호출될 때 지정된 값을 반환하도록 설정하는 메서드
  render(
    <QueryClientProvider client={queryClient}>
      <DataComponent />
    </QueryClientProvider>,
  );

  expect(screen.getByText('Loading...')).toBeInTheDocument();

  await waitFor(() => expect(screen.getByText(/test data/i)).toBeInTheDocument());
});

로딩 상태 확인: expect(screen.getByText('Loading...')).toBeInTheDocument(): 컴포넌트가 로딩 중일 때 "Loading..." 텍스트가 화면에 표시되는지 확인합니다.

데이터 확인: await waitFor(() => expect(screen.getByText(/test data/i)).toBeInTheDocument()): 데이터를 성공적으로 가져왔을 때, mockData의 내용인 "test data"가 화면에 표시되는지 확인합니다.

데이터 업데이트

it('버튼 클릭 시 데이터를 업데이트한다', async () => {
  axios.get.mockResolvedValueOnce({ data: mockData });
  axios.put.mockResolvedValueOnce({ data: updatedData });

  render(
    <QueryClientProvider client={queryClient}>
      <DataComponent />
    </QueryClientProvider>,
  );

  await waitFor(() => expect(screen.getByText(/test data/i)).toBeInTheDocument());

  fireEvent.click(screen.getByText(/Update Data/i));

  await waitFor(() => expect(screen.getByText(/Updated data/i)).toBeInTheDocument());
});

초기 데이터 확인: await waitFor(() => expect(screen.getByText(/test data/i)).toBeInTheDocument()): mockData의 내용인 "test data"가 화면에 표시되는지 확인합니다.

데이터 업데이트: fireEvent.click(screen.getByText(/Update Data/i)): "Update Data" 버튼을 클릭하여 데이터를 업데이트하는 이벤트를 발생시킵니다.

업데이트된 데이터 확인: await waitFor(() => expect(screen.getByText(/Updated data/i)).toBeInTheDocument()): 데이터가 성공적으로 업데이트되었을 때, updatedData의 내용인 "Updated data"가 화면에 표시되는지 확인합니다.