
테스트를 도입하는 목적은 단순하다.
결국 목표는
“코드가 올바르게 동작하는지, 눈으로 클릭 테스트를 하지 않고도 검증할 수 있는 상태”
를 만드는 것이다.
이 구성을 기준으로 한다.
npm install -D vitest @vue/test-utils jsdom
핵심 포인트만 추리면:
jsdom으로 설정@ → src alias를 테스트에서도 동일하게 사용setupFiles로 테스트 공통 초기 설정 파일 등록include로 테스트 파일 위치 지정예시:
// vitest.config.js
import { defineConfig } from 'vitest/config';
import vue from '@vitejs/plugin-vue';
import path from 'node:path';
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
test: {
environment: 'jsdom',
globals: true,
setupFiles: ['./tests/setupTests.js'],
include: ['tests/**/*.test.js'],
},
});
모든 테스트 전에 공통으로 실행되는 초기화 스크립트다.
예를 들어:
등을 넣을 수 있다.
예시(최소 형태):
// tests/setupTests.js
// 필요하면 여기서 공통 설정
console.warn = (...args) => {
console.log('[Vitest warning]', ...args);
};
{
"scripts": {
"test": "vitest",
"test:ui": "vitest --ui"
}
}
npm run test
# 또는 UI 모드
npm run test:ui
Vitest가 tests 폴더 아래의 테스트 파일들을 모두 찾아 실행한다.
npx vitest "tests/components/CourseListItem.test.js"
IDE(WebStorm, VSCode 플러그인 등)에서 직접 실행하는 것도 가능하다.
Vitest는 다음 정보를 보여준다.
describe/it 이름로그만 잘 읽으면 “어떤 동작이 깨졌는지”를 빠르게 파악할 수 있다.
예시 대상: CourseListItem.vue
역할:
isFavorite 여부에 따라 버튼 라벨 변경toggle-favorite 이벤트 emit테스트 포인트:
props 렌더링
조건부 라벨
isFavorite = false → 버튼 텍스트: 즐겨찾기isFavorite = true → 버튼 텍스트: 즐겨찾기 해제이벤트 emit
toggle-favorite 이벤트가 발생하는지course.id가 전달되는지핵심 패턴:
import { mount } from '@vue/test-utils';
import CourseListItem from '@/components/courses/CourseListItem.vue';
describe('CourseListItem', () => {
const createWrapper = (overrides = {}) => {
const defaultProps = {
course: {
id: 1,
title: 'Vue.js 입문',
instructor: '홍길동',
level: 'beginner',
duration: 12,
},
isFavorite: false,
};
return mount(CourseListItem, {
props: { ...defaultProps, ...overrides },
});
};
it('강의 정보가 화면에 표시되어야 한다.', () => {
const wrapper = createWrapper();
expect(wrapper.text()).toContain('Vue.js 입문');
expect(wrapper.text()).toContain('홍길동');
expect(wrapper.text()).toContain('beginner');
expect(wrapper.text()).toContain('12시간');
});
it('버튼 클릭 시 toggle-favorite 이벤트가 발생해야 한다.', async () => {
const wrapper = createWrapper();
const button = wrapper.get('button');
await button.trigger('click');
const emitted = wrapper.emitted('toggle-favorite');
expect(emitted).toBeTruthy();
expect(emitted[0][0]).toBe(1); // 첫 payload = course.id
});
});
이 정도만 테스트해도 컴포넌트의 “계약”이 코드로 문서화된다.
예시 대상: useCourses
역할:
loading / error 상태 관리searchKeyword, levelFilter 기반 필터링API 의존성 제거
vi.mock으로 fetchCourses를 mock 처리해서 항상 같은 더미 데이터를 반환하게 만든다.
테스트용 호스트 컴포넌트
defineComponent 안에서 useCourses()를 호출하고,
wrapper.vm을 통해 ref/computed를 접근한다.
비동기 처리 대기
flushPromises()로 모든 Promise 처리 완료까지 대기await wrapper.vm.$nextTick()으로 DOM 반영까지 기다린 뒤 검증초기 로딩 후
loading === falseerror === nullcourses.length === 3filteredCourses.length === 3검색 필터
"Vue" 검색 시 Vue 관련 강의만 남는지"신사임당" 검색 시 해당 강의만 남는지난이도 필터
levelFilter = 'all' → 전체levelFilter = 'beginner' → beginner만levelFilter = 'advanced' → advanced만이 테스트들이 통과하면:
UI와 상관없이, useCourses의 로직만으로도
데이터 로딩·검색·필터링이 안정적으로 동작한다는 것을 보장할 수 있다.
예시 대상: useCourseStore
역할:
favoriteIds: 즐겨찾기된 강의 ID 목록preferredLevel: 선호 난이도lastViewedCourseId: 마지막으로 본 강의 IDisFavorite, toggleFavorite, setPreferredLevel, setLastViewedCourse)각 테스트마다 새로운 Pinia 인스턴스를 사용한다.
import { setActivePinia, createPinia } from 'pinia';
import { useCourseStore } from '@/stores/courseStore';
beforeEach(() => {
setActivePinia(createPinia());
});
초기 상태
favoriteIds = []preferredLevel = 'beginner'lastViewedCourseId = nulltoggleFavorite
1 추가 → [1]1 토글 → []setPreferredLevel
'beginner' → 'intermediate' → 'advanced'setLastViewedCourse
null → 10 → 99Pinia Store 테스트는
전역 상태 변경 로직이 언제나 예측 가능한 방식으로 동작하는지
를 보장해 준다.
사용자 관점에서 보면 “주소 입력 → 특정 화면” 흐름을 코드로 검증하는 것이다.
실제 브라우저 히스토리가 아니라 메모리 기반 히스토리를 쓴다.
import { createRouter, createMemoryHistory } from 'vue-router';
import HomeView from '@/views/HomeView.vue';
import CoursesBeforeView from '@/views/CoursesBeforeView.vue';
import CoursesAfterView from '@/views/CoursesAfterView.vue';
import PreferencesView from '@/views/PreferencesView.vue';
export function createTestRouter() {
return createRouter({
history: createMemoryHistory(),
routes: [
{ path: '/', name: 'home', component: HomeView },
{ path: '/courses-before', name: 'courses-before', component: CoursesBeforeView },
{ path: '/courses-after', name: 'courses-after', component: CoursesAfterView },
{ path: '/preferences', name: 'preferences', component: PreferencesView },
],
});
}
/ → HomeView/courses-before → CoursesBeforeView/courses-after → CoursesAfterView/preferences → PreferencesView이렇게 실제 App.vue를 router + pinia와 함께 mount한 뒤,
router.push() → await router.isReady() → wrapper.text()로 특정 문구를 찾는 식으로 검증한다.
예시 시나리오:
/preferences는 로그인된 사용자만 접근 가능하다.meta: { requiresAuth: true } 설정beforeEach에서 로그인 여부에 따라 리다이렉트테스트에서는 실제 authStore 대신 간단한 플래그로 대체해서 가드 로직만 검증할 수 있다.
/preferences 진입 시 /로 리다이렉트/preferences 접근 허용예시 대상: CoursesAfterView
시나리오:
useCourses에서 넘어온 강의 목록이 화면에 렌더링된다.
각 카드의 “즐겨찾기” 버튼을 누르면
courseStore.toggleFavorite(id) 호출courseStore.setLastViewedCourse(id) 호출그 결과 Pinia store의 상태가 기대대로 바뀐다.
useCourses는 실제 구현 대신 vi.mock으로 간단한 더미 데이터를 반환하게 만든다.테스트에서 확인하는 것:
CoursesAfterView를 Pinia와 함께 mount
초기 favoriteIds = [], lastViewedCourseId = null
첫 번째 강의 카드의 버튼 클릭
결과:
favoriteIds = [1]lastViewedCourseId = 1이 한 번의 테스트로
까지 전부 연결되어 있는지 한 번에 검증할 수 있다.
초중급 프론트엔드 개발 기준으로 현실적인 전략만 뽑으면 다음 네 가지다.
가장 중요하다.
→ 검색, 필터, 정렬, 상태 변경 로직은 웬만하면 Composable/Store 단위로 테스트를 작성해 두는 게 좋다.
정리하면,
이 네 가지를 각각 알맞은 수준에서 테스트하면, 리팩토링과 기능 추가가 훨씬 안전해진다.