단위 테스트 및 통합 테스트 코드 작성 기록
이제부터 테스트 코드에 대해서 이야기 해보겠습니다. 테스트 코드는 AAA 패턴으로 작성했습니다. Arrange(준비), Act(실행), Assert(검증) 순서이며 테스트들의 구조들을 일관되게 작성함으로서 가독성과 유지보수성을 늘렸습니다.
먼저 테스트 코드가 정상작동하는지 확인하기 위해 util 함수들의 단위 테스트코드를 작성했습니다.
단위 테스트란 가장 작은 소프트웨어를 실행하여 예상대로 동작하는지 확인하는 테스트이며 보통 함수 단위로 테스트를 합니다.
export const createFormattedTime = (
date: Date,
format = 'YYYY.MM.DD',
): string => {
const formatReplacements: Record<string, string> = {
YYYY: String(date.getFullYear()),
YY: String(date.getFullYear()).substring(2, 4),
MM: String(date.getMonth() + 1).padStart(2, '0'),
DD: String(date.getDate()).padStart(2, '0'),
hh: String(date.getHours()).padStart(2, '0'),
mm: String(date.getMinutes()).padStart(2, '0'),
};
const formattedTime = format.replace(
/YYYY|YY|MM|DD|hh|mm/g,
match => formatReplacements[match],
);
return formattedTime;
};
createFormattedTime함수는 date객체와 원하는 포맷의 문자열을 입력받아 포맷대로 날짜 문자열을 반환하는 함수입니다.
import { createFormattedTime } from './dateFormat';
describe('createFormattedTime 함수', () => {
test('첫번째 매개변수로 날짜 입력했을 때 YYYY.MM.DD 형식으로 된 문자열 반환', () => {
const date = new Date('2022-01-01T12:34:56');
const formattedTime = createFormattedTime(date);
expect(formattedTime).toBe('2022.01.01');
});
test('두번째 매개변수로 사용자 형식을 입력했을 때 사용자 정의 형식 정상적으로 포맷팅되는지 확인', () => {
type TestCase = { format: string; result: string };
const date = new Date('2022-01-01T12:34:56');
const testCases: TestCase[] = [
{
format: 'YYYY년 MM월 DD일 hh:mm',
result: '2022년 01월 01일 12:34',
},
//...
{
format: 'hh:mm',
result: '12:34',
},
];
testCases.forEach(({ format, result }) => {
const formattedTime = createFormattedTime(date, format);
expect(formattedTime).toBe(result);
});
});
});
단위테스트를 통과한다고 해서 프로그램이 잘 돌아간다고 확신할 수 없습니다. 바퀴가 잘 굴러간다고 자동차가 잘 동작한다고 확신할 수 없는 것처럼요.
마찬가지로 input들이 정상작동한다고 form이 정상적으로 작동한다는 보장할 수는 없었습니다. 따라서 각각의 form의 통합테스트가 필요하다고 판단했습니다.
통합 테스트는 단위 테스트보다 더 큰 동작을 달성하기 위해 여러 모듈들을 모아 이들이 의도대로 협력하는지 확인하는 테스트입니다.
작성한 테스트 코드 중 이름 변경 폼 테스트를 일부 변형해서 가져왔습니다.
import { screen } from '@testing-library/react';
import { SettingName } from '../SettingName';
import render from '@/utils/test/render';
const navigateFn = vi.fn();
vi.mock('react-router-dom', async () => {
const original = await vi.importActual('react-router-dom');
return {
...original,
useNavigate: () => navigateFn,
};
});
...
vi.mock(회사 공통 모듈, async () => {
...
});
vi.mock(전역 스토어, async () => ({
...
}));
테스트가 진행되기 전 먼저 mocking 했습니다. 외부 라이브러리들이나 스토어들은 mocking한 후 함수의 호출만을 테스트했습니다.
...
describe('전역 window 객체의 env.ENV1가 A일 경우', () => {
beforeAll(() => {
vi.stubGlobal('env', {
ENV1: 'A',
});
afterAll(() => {
vi.clearAllMocks();
});
});
...
})
...
또한 저희 회사에서는 전역 window 객체에 입력한 환경변수 값에 따라 다른 UI를 보여주었습니다.
이를 위해서 window 객체의 mocking이 필요했는데 이는 Vitest에서 제공하는 StubGlobal을 이용해서 진행했습니다.
describe('이름 변경 페이지 통합 테스트', () => {
...
it('페이지가 렌더링된 후 이름 변경 input이 포커스 된다.', async () => {
await render(<SettingName />);
expect(screen.getByPlaceholderText(MOCK_NAME)).toHaveFocus();
});
it('페이지가 렌더링된 후 이름 변경 input에서 원래 유저의 이름을 보여준다.', async () => {
await render(<SettingName />);
expect(screen.getByDisplayValue(MOCK_NAME)).toBeInTheDocument();
});
it('이름 변경 input값이 비어있다면 placeholder로 원래 유저의 이름을 보여준다.', async () => {
await render(<SettingName />);
expect(screen.getByPlaceholderText(MOCK_NAME)).toBeInTheDocument();
});
it('페이지가 렌더링 됐을 때 현재 이름과 변경할 이름이 같으므로 저장 버튼이 비활성화된다', async () => {
await render(<SettingName />);
const submitButton = screen.getByRole('button', { name: BUTTON_TEXT });
expect(submitButton).toBeDisabled();
});
//...
});
describe('이름 변경 페이지 통합 테스트', () => {
//...
it('이름이 비어있는 채로 저장 버튼을 누르면 'EMPTY_NAME' 경고문구가 노출된다.', async () => {
const { user } = await render(<SettingName />);
const updateNameTextField = screen.getByPlaceholderText(MOCK_NAME);
const submitButton = screen.getByRole('button', { name: BUTTOM_TEXT });
await user.clear(updateNameTextField);
await user.click(submitButton);
expect(
screen.getByText(EMPTY_NAME),
).toBeInTheDocument();
});
it('이름은 10글자 이상 입력 시 초과되는 글자는 입력되지 않는다', async () => {
const { user } = await render(<SettingName />);
const updateNameTextField = screen.getByPlaceholderText(MOCK_NAME);
await user.clear(updateNameTextField);
await user.type(updateNameTextField, '01234567890');
expect(screen.getByDisplayValue('0123456789')).toBeInTheDocument();
});
it('현재 이름과 변경할 이름이 같은 경우 저장 버튼이 비활성화된다', async () => {
const { user } = await render(<SettingName />);
const updateNameTextField = screen.getByPlaceholderText(MOCK_NAME);
const submitButton = screen.getByRole('button', { name: BUTTON_TEXT });
await user.clear(updateNameTextField);
await user.type(updateNameTextField, MOCK_NAME);
expect(submitButton).toBeDisabled();
});
//..
})
describe('이름 변경 페이지 통합 테스트', () => {
//...
it('유효성에 맞는 이름을 입력하고 제출하면 updateUser 함수가 호출된다.', async () => {
const { user } = await render(<SettingName />);
const updateNameTextField = screen.getByPlaceholderText(MOCK_NAME);
const submitButton = screen.getByRole('button', { name: BUTTON_TEXT });
await user.clear(updateNameTextField);
await user.type(updateNameTextField, MOCK_UPDATE_NAME);
await user.click(submitButton);
expect(updateUserFn).toHaveBeenNthCalledWith(1, {
id: MOCK_ID,
name: MOCK_UPDATE_NAME,
});
});
it('이름 변경이 성공하면 -1 경로로 navigate함수를 호출한다.', async () => {
const { user } = await render(<SettingName />);
const updateNameTextField = screen.getByPlaceholderText(MOCK_NAME);
const submitButton = screen.getByRole('button', { name: BUTTON_TEXT });
await user.clear(updateNameTextField);
await user.type(updateNameTextField, MOCK_UPDATE_NAME);
await user.click(submitButton);
expect(navigateFn).toHaveBeenNthCalledWith(1, -1);
});
});
여기까지 테스트 코드를 작성한 경험에 대해 이야기 해보았습니다. 다음 게시글에서는 테스트 코드를 작성 한 후 react-hook-form + zod를 이용해서 유효성 검사를 진행했던 경험을 공유하려고 합니다.