개요
팀과 프로덕트가 늘어나면서 다른 부서에서도 우리 팀이 만든 모노레포를 공통 플랫폼으로 사용하게 되었습니다. 이에 따라 공통 컴포넌트의 테스트 방침을 정하고, 테스트 코드를 도입하여 공통 컴포넌트를 쉽고 안전하게 수정할 수 있도록 하고자 했습니다.
기존에도 공통 컴포넌트에 테스트 코드가 있었지만, 명확한 방침 없이 작성되어 내용이 제각각이었습니다. 이번에 방침을 정해 테스트 코드의 작성 기준을 통일하고자 했습니다.
컴포넌트의 테스트 대상
React 컴포넌트는 Props, State, 그리고 Props와 State에 의해 화면에 표시되는 View, 표시된 View와 사용자의 인터랙션을 담당하는 Event로 구성됩니다.

여기서 테스트 대상은 아래 두 가지입니다.
- View (Props, State에 의한 Business Logic)
Props, State => Business Logic => View - Event (Event Handler 내의 Business Logic)
Event => Business Logic => State => Business Logic => View Event => Business Logic => Parent Event Handler
테스트 방침을 정해 이 부분들을 효과적으로 테스트할 수 있도록 해야 합니다.
테스트 방침
테스트 대상을 기반으로 아래와 같은 테스트 방침을 정했습니다.
- Props를 변경하면서 View 확인
- Props, State(Props에 의해 초기화되는 State) 변경 => Business Logic => View 확인
- 이벤트 발생시키면서 View 또는 Event Handler 확인
- Event 발화 => Business Logic => State => Business Logic => View 확인
- Event 발화 => Business Logic => Parent Event Handler 확인
- 접근성(Accessibility) 테스트
테스트 방법
컴포넌트 테스트는 Jest, Storybook의 Interaction 테스트, Vitest로 작성할 수 있습니다. 우선 각각의 방법을 살펴보고 팀에 가장 적합한 방법을 선택하기로 했습니다.
Jest
Jest + Puppeteer를 사용한 VRT 환경 구축의 자세한 내용은 [React] Jest + Puppeteer로 VRT 구축하기를 참고해 주세요.
1. Props를 변경하면서 View 확인
스냅샷 테스트를 활용하여 Props 변경에 따른 View를 검증합니다.
it('should match snapshot for primary variant', async () => {
await expectToMatchVRTSnapshot(
<Button variant="primary">Primary Button</Button>,
'button-primary'
);
});
2. 이벤트 테스트
이벤트를 발생시키고 그에 따른 상태 변화 및 View 변경을 검증합니다.
test('displays custom fetchErrorText', async () => {
const error = new Error('Network error');
mockFetchChildNodesFunction.mockRejectedValueOnce(error);
const customErrorText = {
title: '커스텀 에러',
description: '커스텀 설명',
btnLabel: '재시도',
};
render(
<ExpandableTreeList
topTreeNodes={sampleTopTreeNodes}
fetchErrorText={customErrorText}
fetchChildNodesFunction={mockFetchChildNodesFunction}
/>
);
const expandButton = screen.getAllByRole('button')[0];
fireEvent.click(expandButton);
await waitFor(() => {
expect(screen.getByText('커스텀 에러')).toBeInTheDocument();
expect(screen.getByText('커스텀 설명')).toBeInTheDocument();
expect(screen.getByText('재시도')).toBeInTheDocument();
});
});
3. 접근성 테스트 (jest-axe)
test('has no accessibility violations', async () => {
const { container } = render(
<ExpandableTreeList
topTreeNodes={sampleTopTreeNodes}
sectionLabel="접근성 테스트"
countLabel="${count}건"
fetchChildNodesFunction={mockFetchChildNodesFunction}
onRemove={mockOnRemove}
/>
);
const results = await axe(container, {
rules: {
'button-name': { enabled: false },
},
});
expect(results).toHaveNoViolations();
});
Storybook
Storybook Test Runner를 사용한 VRT + 접근성 테스트 환경 구축의 자세한 내용은 [React] Storybook Test Runner로 VRT + 접근성 테스트 구축하기를 참고해 주세요.
1. Props를 변경하면서 View 확인 (Storybook Snapshot)
export const RenderingTopTreeNodes: Story = {
name: 'Rendering: 트리 노드 표시',
render: () => (
<div style={{ height: '25rem' }}>
<ExpandableTreeList
topTreeNodes={sampleTopTreeNodes}
fetchChildNodesFunction={createMockFetchFunction()}
/>
</div>
),
parameters: {
chromatic: { disableSnapshot: false },
},
};
2. 이벤트 테스트 (Storybook Interaction)
export const EventDisplayChildNodes: Story = {
name: 'Event: 페치 후 자식 노드 표시',
render: () => (
<div style={{ height: '25rem' }}>
<ExpandableTreeList
topTreeNodes={sampleTopTreeNodes}
fetchChildNodesFunction={createMockFetchFunction()}
/>
</div>
),
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const expandButton = canvas.getAllByRole('button')[0];
await userEvent.click(expandButton);
await new Promise((resolve) => setTimeout(resolve, 100));
expect(canvas.getByText('Child 1')).toBeInTheDocument();
expect(canvas.getByText('Child 2')).toBeInTheDocument();
},
};
3. 접근성 테스트 (Storybook a11y)
export const A11yTest: Story = {
name: 'Accessibility: 접근성 테스트',
tags: ['a11y-test'],
render: () => (
<div style={{ height: '25rem' }}>
<ExpandableTreeList
topTreeNodes={sampleTopTreeNodes}
sectionLabel="접근성 테스트"
countLabel="${count}건"
fetchChildNodesFunction={createMockFetchFunction()}
onRemove={fn()}
/>
</div>
),
parameters: {
chromatic: {
disableSnapshot: true,
},
a11y: {
config: {
rules: [
{
id: 'button-name',
enabled: false,
},
],
},
},
},
};
Vitest
Vitest를 사용한 VRT + 접근성 테스트 환경 구축의 자세한 내용은 [React] Vitest로 컴포넌트 VRT + 접근성 테스트 환경 구축을 참고해 주세요.
1. Props를 변경하면서 View 확인
Vitest는 기본적으로 VRT(Visual Regression Testing)를 지원합니다.
it('renders with text content', async () => {
render(<Button>Click me</Button>);
await expect
.element(page.getByRole('button'))
.toMatchScreenshot('button-children-text.png');
});
2. 이벤트 테스트
it('calls onClick handler when clicked', async () => {
const onClick = vi.fn();
render(<Button onClick={onClick}>Click me</Button>);
await userEvent.click(screen.getByRole('button'));
expect(onClick).toHaveBeenCalledTimes(1);
});
3. 접근성 테스트 (axe-core)
it('should have no accessibility violations', async () => {
const { container } = render(<Button>Accessible Button</Button>);
const results = await axe.run(container, axeConfig);
expect(results.violations).toHaveLength(0);
});
비교 및 결론
| Jest | Storybook | Vitest | |
|---|---|---|---|
| Props 변경에 따른 View 확인 (VRT) | O (VRT 테스트는 별도 파일로 작성 필요) | O (프로덕트 테스트 코드와 작성 방식이 다름) | O |
| 이벤트 테스트 | O | O | O |
| 접근성 테스트 | O | O | O |
결론: Vitest를 선택한 이유
세 가지 방법 모두 Props 변경에 따른 View 확인, 이벤트 테스트, 접근성 테스트가 가능했습니다. 하지만, 다음과 같은 이유로 Vitest가 가장 적합하다고 판단했습니다.
- Storybook: 프로덕트의 테스트 코드 작성 방식과 다르고, Story 내용이 방대해져 관리가 어려워집니다. Storybook은 디자인과 구현 확인 용도로 활용하는 것이 적합합니다.
- Jest: VRT 테스트는 가능하지만, 테스트 파일을 분리해야 하는 제약이 있습니다.
- Vitest: 기본적으로 VRT를 제공하고, 프로덕트 레벨의 View 테스트와 동일한 기술 스택을 사용하므로 가장 적합합니다.
따라서 우리 팀에서는 Vitest로 공통 컴포넌트의 테스트 코드를 작성하는 것으로 결정했습니다.
참고: Vitest - Visual Regression Testing
정리
모노레포 환경에서 React 공통 컴포넌트의 테스트 방침을 정하는 과정을 공유했습니다. Props/State에 의한 View 테스트, 이벤트 테스트, 접근성 테스트를 Jest, Storybook, Vitest로 비교하고, 최종적으로 Vitest를 선택한 이유를 공유했습니다.
앞으로는 Vitest로 작성된 테스트 코드를 통해 다른 팀들도 공통 컴포넌트를 쉽고 안전하게 수정할 수 있을 것으로 기대합니다. 또한, 테스트 코드를 작성하는 기준이 명확해져 일관된 테스트 코드가 작성될 것입니다.
기존 Jest 프로젝트에 Vitest를 점진적으로 도입하는 구체적인 방법은 [React] Jest 프로젝트에 Vitest 도입하기 - View 테스트 분리 전략을, Vitest VRT + 접근성 테스트 환경 구축에 대해서는 [React] Vitest로 컴포넌트 VRT + 접근성 테스트 환경 구축을 참고해주세요.
이렇게 테스트 방침을 정하면 생성 AI를 활용하여 테스트 코드도 쉽게 작성할 수 있습니다. 여러분도 팀에 맞는 테스트 방침을 정하고, 생성 AI를 활용하여 테스트 코드를 쉽고 빠르게 작성해 보세요!
제 블로그가 도움이 되셨나요? 하단의 댓글을 달아주시면 저에게 큰 힘이 됩니다!
앱 홍보
Deku가 개발한 앱을 한번 사용해보세요.Deku가 개발한 앱은 Flutter로 개발되었습니다.관심있으신 분들은 앱을 다운로드하여 사용해 주시면 정말 감사하겠습니다.