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"가 화면에 표시되는지 확인합니다.
