161 lines
4.1 KiB
Markdown
161 lines
4.1 KiB
Markdown
---
|
|
name: dev-unit-test
|
|
description: |
|
|
Vue 3 컴포넌트를 받아 Vitest + Vue Test Utils 기반의 단위 테스트를 자동 생성합니다.
|
|
Props, Emits, 슬롯, 사용자 인터랙션, 비동기 동작을 모두 커버합니다.
|
|
|
|
다음 상황에서 반드시 사용하세요:
|
|
- "이 컴포넌트 단위 테스트 작성해줘", "테스트 코드 만들어줘"
|
|
- "Vitest 어떻게 써?", "Vue Test Utils 사용 방법"
|
|
- 컴포넌트 완성 후 커버리지를 확보해야 할 때
|
|
---
|
|
|
|
# 단위 테스트 생성 (dev-unit-test)
|
|
|
|
`.vue` 컴포넌트 → Vitest + Vue Test Utils 단위 테스트 자동 생성.
|
|
|
|
## 언제 사용하는가
|
|
|
|
- 컴포넌트 개발 완료 후 테스트 코드를 작성할 때
|
|
- TDD 방식으로 테스트를 먼저 작성할 때
|
|
- 팀 테스트 커버리지 기준(80%↑)을 달성해야 할 때
|
|
|
|
## 입력
|
|
|
|
- 테스트할 `.vue` 파일 경로
|
|
- 테스트 케이스 범위 (없으면 자동 도출)
|
|
|
|
---
|
|
|
|
## 작업 순서
|
|
|
|
### Phase 1: 컴포넌트 분석
|
|
|
|
1. 대상 `.vue` 파일을 읽어 아래 항목을 파악한다.
|
|
- Props 목록 (타입, 기본값, 필수 여부)
|
|
- Emits 목록 (이벤트 이름, 페이로드 타입)
|
|
- 슬롯 유무
|
|
- 외부 의존성 (composables, store, $fetch)
|
|
- 인터랙션 (버튼 클릭, 입력, 폼 제출)
|
|
- 비동기 동작 (API 호출, 로딩 상태)
|
|
- 조건부 렌더링 (`v-if`, `v-show`)
|
|
|
|
2. 테스트 케이스 목록을 도출한다.
|
|
|
|
### Phase 2: 테스트 설정
|
|
|
|
```ts
|
|
// vitest.config.ts (없으면 생성 안내)
|
|
import { defineConfig } from 'vitest/config'
|
|
import vue from '@vitejs/plugin-vue'
|
|
|
|
export default defineConfig({
|
|
plugins: [vue()],
|
|
test: {
|
|
environment: 'jsdom',
|
|
globals: true,
|
|
},
|
|
})
|
|
```
|
|
|
|
### Phase 3: 테스트 파일 생성
|
|
|
|
파일 위치: 컴포넌트와 동일 디렉토리 또는 `__tests__/` 폴더.
|
|
파일명: `<ComponentName>.spec.ts`
|
|
|
|
```ts
|
|
import { describe, it, expect, vi } from 'vitest'
|
|
import { mount } from '@vue/test-utils'
|
|
import ComponentName from './ComponentName.vue'
|
|
|
|
describe('ComponentName', () => {
|
|
// Phase 4 케이스 작성
|
|
})
|
|
```
|
|
|
|
### Phase 4: 테스트 케이스 작성 패턴
|
|
|
|
#### Props 검증
|
|
```ts
|
|
it('title prop을 렌더링한다', () => {
|
|
const wrapper = mount(ComponentName, {
|
|
props: { title: '테스트 제목' }
|
|
})
|
|
expect(wrapper.find('h2').text()).toBe('테스트 제목')
|
|
})
|
|
|
|
it('필수 prop 누락 시 기본값을 사용한다', () => {
|
|
const wrapper = mount(ComponentName)
|
|
expect(wrapper.find('[data-testid="label"]').text()).toBe('기본값')
|
|
})
|
|
```
|
|
|
|
#### Emits 검증
|
|
```ts
|
|
it('버튼 클릭 시 click 이벤트를 emit한다', async () => {
|
|
const wrapper = mount(ComponentName, { props: { id: '1' } })
|
|
await wrapper.find('button').trigger('click')
|
|
expect(wrapper.emitted('click')?.[0]).toEqual(['1'])
|
|
})
|
|
```
|
|
|
|
#### 조건부 렌더링
|
|
```ts
|
|
it('isLoading이 true이면 스피너를 표시한다', () => {
|
|
const wrapper = mount(ComponentName, { props: { isLoading: true } })
|
|
expect(wrapper.find('[aria-busy="true"]').exists()).toBe(true)
|
|
expect(wrapper.find('[data-testid="content"]').exists()).toBe(false)
|
|
})
|
|
```
|
|
|
|
#### 비동기 / 스토어 Mock
|
|
```ts
|
|
import { createTestingPinia } from '@pinia/testing'
|
|
|
|
it('마운트 시 fetchProducts를 호출한다', async () => {
|
|
const wrapper = mount(ComponentName, {
|
|
global: {
|
|
plugins: [createTestingPinia({ createSpy: vi.fn })]
|
|
}
|
|
})
|
|
const store = useProductStore()
|
|
expect(store.fetchProducts).toHaveBeenCalledOnce()
|
|
})
|
|
```
|
|
|
|
#### 슬롯
|
|
```ts
|
|
it('default 슬롯 콘텐츠를 렌더링한다', () => {
|
|
const wrapper = mount(ComponentName, {
|
|
slots: { default: '<span>슬롯 내용</span>' }
|
|
})
|
|
expect(wrapper.find('span').text()).toBe('슬롯 내용')
|
|
})
|
|
```
|
|
|
|
---
|
|
|
|
## 출력 형식
|
|
|
|
```
|
|
## 단위 테스트: <ComponentName>.spec.ts
|
|
|
|
### 테스트 케이스 목록
|
|
1. [렌더링] 기본 렌더링 성공
|
|
2. [Props] title prop 표시
|
|
3. [Emits] 버튼 클릭 시 emit
|
|
4. [조건부] isLoading 상태 처리
|
|
...
|
|
|
|
### 코드
|
|
\`\`\`ts
|
|
[전체 spec 파일]
|
|
\`\`\`
|
|
|
|
### 실행 방법
|
|
\`\`\`bash
|
|
npx vitest run <파일경로>
|
|
npx vitest --coverage # 커버리지 확인
|
|
\`\`\`
|
|
```
|