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