테스트 — Vitest와 Testing Library로 Svelte 컴포넌트 테스트하기
테스트 없는 코드는 "동작하는 것 같은" 코드입니다 — 테스트가 있어야 "동작하는" 코드입니다.
개념 정의
컴포넌트 테스트 는 Svelte 컴포넌트가 올바르게 렌더링되고, 사용자 인터랙션에 적절히 반응하는지 검증합니다. Vitest(테스트 러너) + Svelte Testing Library(렌더링/쿼리) 조합이 표준입니다.
설정
npm install -D vitest @testing-library/svelte @testing-library/jest-dom jsdom @sveltejs/vite-plugin-svelte
// vite.config.js
import { defineConfig } from 'vitest/config';
import { svelte } from '@sveltejs/vite-plugin-svelte';
export default defineConfig({
plugins: [svelte({ hot: !process.env.VITEST })],
test: {
include: ['src/**/*.{test,spec}.{js,ts}'],
globals: true,
environment: 'jsdom',
setupFiles: ['./src/tests/setup.js'],
},
});
// src/tests/setup.js
import '@testing-library/jest-dom/vitest';
기본 테스트
<!-- Counter.svelte -->
<script>
let count = $state(0);
</script>
<button onclick={() => count++}>카운트: {count}</button>
// Counter.test.js
import { render, screen, fireEvent } from '@testing-library/svelte';
import { describe, it, expect } from 'vitest';
import Counter from './Counter.svelte';
describe('Counter', () => {
it('초기 카운트는 0이어야 한다', () => {
render(Counter);
expect(screen.getByRole('button')).toHaveTextContent('카운트: 0');
});
it('클릭하면 카운트가 증가해야 한다', async () => {
render(Counter);
const button = screen.getByRole('button');
await fireEvent.click(button);
expect(button).toHaveTextContent('카운트: 1');
await fireEvent.click(button);
expect(button).toHaveTextContent('카운트: 2');
});
});
Props 테스트
<!-- Greeting.svelte -->
<script>
let { name, greeting = '안녕하세요' } = $props();
</script>
<h1>{greeting}, {name}님!</h1>
import { render, screen } from '@testing-library/svelte';
import Greeting from './Greeting.svelte';
describe('Greeting', () => {
it('이름과 기본 인사말을 표시해야 한다', () => {
render(Greeting, { props: { name: '홍길동' } });
expect(screen.getByRole('heading')).toHaveTextContent('안녕하세요, 홍길동님!');
});
it('커스텀 인사말을 표시해야 한다', () => {
render(Greeting, { props: { name: '김철수', greeting: '반갑습니다' } });
expect(screen.getByRole('heading')).toHaveTextContent('반갑습니다, 김철수님!');
});
});
사용자 인터랙션 테스트
import { render, screen } from '@testing-library/svelte';
import userEvent from '@testing-library/user-event';
import SearchBar from './SearchBar.svelte';
describe('SearchBar', () => {
it('입력 후 엔터를 누르면 검색 콜백이 호출되어야 한다', async () => {
const user = userEvent.setup();
const onSearch = vi.fn();
render(SearchBar, { props: { onSearch } });
const input = screen.getByPlaceholderText('검색어 입력');
await user.type(input, 'Svelte');
await user.keyboard('{Enter}');
expect(onSearch).toHaveBeenCalledWith('Svelte');
});
});
비동기 테스트
import { render, screen, waitFor } from '@testing-library/svelte';
import UserList from './UserList.svelte';
// fetch 모킹
beforeEach(() => {
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: () => Promise.resolve([
{ id: '1', name: '홍길동' },
{ id: '2', name: '김철수' },
]),
});
});
describe('UserList', () => {
it('사용자 목록을 로드하여 표시해야 한다', async () => {
render(UserList);
// 로딩 상태 확인
expect(screen.getByText('로딩 중...')).toBeInTheDocument();
// 데이터 로드 후 확인
await waitFor(() => {
expect(screen.getByText('홍길동')).toBeInTheDocument();
expect(screen.getByText('김철수')).toBeInTheDocument();
});
});
});
접근성 테스트
import { render } from '@testing-library/svelte';
import { axe, toHaveNoViolations } from 'jest-axe';
import LoginForm from './LoginForm.svelte';
expect.extend(toHaveNoViolations);
it('접근성 위반이 없어야 한다', async () => {
const { container } = render(LoginForm);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
면접 포인트
- "컴포넌트 테스트에서 가장 중요한 원칙은?": 구현 세부사항이 아닌 사용자 관점에서 테스트합니다. 내부 상태를 직접 확인하는 대신, 화면에 보이는 텍스트나 역할(role)을 기준으로 쿼리합니다.
- "Testing Library의 쿼리 우선순위는?":
getByRole>getByLabelText>getByPlaceholderText>getByText>getByTestId. 접근성이 좋은 쿼리를 우선 사용합니다.
정리
Svelte 컴포넌트 테스트는 "사용자가 보는 것을 테스트한다"가 핵심입니다. Vitest로 빠르게 실행하고, Testing Library로 사용자 관점에서 검증하면, 리팩토링에도 깨지지 않는 견고한 테스트를 작성할 수 있습니다.
댓글 로딩 중...