가이드로 돌아가기
테스트
가이드
HUA UX 컴포넌트와 훅을 테스트하는 방법을 알아보세요.
빠른 시작
pnpm add -D vitest @testing-library/react @testing-library/jest-dom jsdomVitest
테스트 러너
RTL
React Testing Library
jest-axe
접근성 테스트
Vitest 설정tsx
1// vitest.config.ts2import { 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.ts2import '@testing-library/jest-dom';3import { vi } from 'vitest';4 5// Mock IntersectionObserver for useMotion6global.IntersectionObserver = vi.fn().mockImplementation(() => ({7 observe: vi.fn(),8 unobserve: vi.fn(),9 disconnect: vi.fn(),10}));11 12// Mock matchMedia for dark mode13Object.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.tsx2import { 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.tsx2import { 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 022 expect(result.current.style.opacity).toBe(0);23 });24});i18n 테스트tsx
1// i18n.test.tsx2import { 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.tsx2import { 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에서 상태를 초기화하세요.