Skip to content
Docs
가이드로 돌아가기

테스트

가이드

HUA UX 컴포넌트와 훅을 테스트하는 방법을 알아보세요.

빠른 시작

pnpm add -D vitest @testing-library/react @testing-library/jest-dom jsdom
Vitest
테스트 러너
RTL
React Testing Library
jest-axe
접근성 테스트
Vitest 설정tsx
1// vitest.config.ts
2import { defineConfig } from 'vitest/config';
3import react from '@vitejs/plugin-react';
4 
5export default defineConfig({
6 plugins: [react()],
7 test: {
8 environment: 'jsdom',
9 globals: true,
10 setupFiles: './test/setup.ts',
11 },
12});
테스트 셋업 파일tsx
1// test/setup.ts
2import '@testing-library/jest-dom';
3import { vi } from 'vitest';
4 
5// Mock IntersectionObserver for useMotion
6global.IntersectionObserver = vi.fn().mockImplementation(() => ({
7 observe: vi.fn(),
8 unobserve: vi.fn(),
9 disconnect: vi.fn(),
10}));
11 
12// Mock matchMedia for dark mode
13Object.defineProperty(window, 'matchMedia', {
14 value: vi.fn().mockImplementation((query) => ({
15 matches: false,
16 media: query,
17 addEventListener: vi.fn(),
18 removeEventListener: vi.fn(),
19 })),
20});
컴포넌트 테스트tsx
1// Button.test.tsx
2import { render, screen, fireEvent } from '@testing-library/react';
3import { Button } from '@hua-labs/hua';
4import { describe, it, expect, vi } from 'vitest';
5 
6describe('Button', () => {
7 it('renders correctly', () => {
8 render(<Button>Click me</Button>);
9 expect(screen.getByRole('button')).toHaveTextContent('Click me');
10 });
11 
12 it('handles click events', () => {
13 const handleClick = vi.fn();
14 render(<Button onClick={handleClick}>Click</Button>);
15 
16 fireEvent.click(screen.getByRole('button'));
17 expect(handleClick).toHaveBeenCalledTimes(1);
18 });
19 
20 it('applies variant styles', () => {
21 render(<Button variant="outline">Outline</Button>);
22 expect(screen.getByRole('button')).toHaveClass('border');
23 });
24});
훅 테스트tsx
1// useMotion.test.tsx
2import { renderHook } from '@testing-library/react';
3import { useMotion } from '@hua-labs/hua/framework';
4import { describe, it, expect } from 'vitest';
5 
6describe('useMotion', () => {
7 it('returns ref and style', () => {
8 const { result } = renderHook(() =>
9 useMotion({ type: 'fadeIn', duration: 500 })
10 );
11 
12 expect(result.current.ref).toBeDefined();
13 expect(result.current.style).toBeDefined();
14 });
15 
16 it('applies initial opacity for fadeIn', () => {
17 const { result } = renderHook(() =>
18 useMotion({ type: 'fadeIn' })
19 );
20 
21 // Before intersection, opacity should be 0
22 expect(result.current.style.opacity).toBe(0);
23 });
24});
i18n 테스트tsx
1// i18n.test.tsx
2import { render, screen } from '@testing-library/react';
3import { I18nProvider, useTranslation } from '@hua-labs/i18n-core';
4import { describe, it, expect } from 'vitest';
5 
6const TestComponent = () => {
7 const { t, currentLanguage } = useTranslation();
8 return (
9 <div>
10 <span data-testid="lang">{currentLanguage}</span>
11 <span data-testid="text">{t('common:hello')}</span>
12 </div>
13 );
14};
15 
16const config = {
17 defaultLanguage: 'ko',
18 supportedLanguages: [
19 { code: 'ko', name: 'Korean', nativeName: '한국어' },
20 { code: 'en', name: 'English', nativeName: 'English' },
21 ],
22 namespaces: ['common'],
23 loadPath: '/translations/{{lng}}/{{ns}}.json',
24};
25 
26describe('useTranslation', () => {
27 it('provides current language', () => {
28 render(
29 <I18nProvider config={config}>
30 <TestComponent />
31 </I18nProvider>
32 );
33 
34 expect(screen.getByTestId('lang')).toHaveTextContent('ko');
35 });
36});
접근성 테스트tsx
1// accessibility.test.tsx
2import { render } from '@testing-library/react';
3import { axe, toHaveNoViolations } from 'jest-axe';
4import { Button, Card } from '@hua-labs/hua';
5import { Input } from '@hua-labs/ui';
6import { describe, it, expect } from 'vitest';
7 
8expect.extend(toHaveNoViolations);
9 
10describe('Accessibility', () => {
11 it('Button has no a11y violations', async () => {
12 const { container } = render(<Button>Click me</Button>);
13 const results = await axe(container);
14 expect(results).toHaveNoViolations();
15 });
16 
17 it('Input with label has no a11y violations', async () => {
18 const { container } = render(
19 <div>
20 <label htmlFor="email">Email</label>
21 <Input id="email" type="email" />
22 </div>
23 );
24 const results = await axe(container);
25 expect(results).toHaveNoViolations();
26 });
27 
28 it('Card with proper heading structure', async () => {
29 const { container } = render(
30 <Card>
31 <h2>Card Title</h2>
32 <p>Card content</p>
33 </Card>
34 );
35 const results = await axe(container);
36 expect(results).toHaveNoViolations();
37 });
38});

테스트 팁

모킹 전략

IntersectionObserver, matchMedia 등 브라우저 API는 테스트 환경에서 모킹이 필요합니다.

스냅샷 테스트

UI 변경을 추적하려면 스냅샷 테스트를 활용하세요. 단, 과도한 사용은 피하세요.

테스트 격리

각 테스트는 독립적으로 실행되어야 합니다. beforeEach에서 상태를 초기화하세요.