# 2026년 Vue 3 테스팅: Vitest, Vue Test Utils 그리고 면접 질문 > 2026년 Vue 테스팅 실전 가이드. Vitest 설정, Vue Test Utils로 컴포넌트 마운트, 컴포저블과 Pinia 스토어 테스트, API 모킹, 커버리지 측정, 그리고 채용 팀이 실제로 던지는 면접 질문까지 다룹니다. - Published: 2026-06-21 - Updated: 2026-07-06 - Author: SharpSkill - Tags: vue, testing, vitest, vue-test-utils, interview - Reading time: 10 min --- Vue 테스팅은 자신 있는 리팩터링과 불안한 추측을 갈라놓습니다. 2026년 현재 관련 도구 생태계는 두 라이브러리를 중심으로 정리되었습니다. 러너 역할을 하는 [Vitest](https://vitest.dev/guide/)와 컴포넌트 마운트를 담당하는 [Vue Test Utils](https://test-utils.vuejs.org/)입니다. 이 가이드는 이 스택을 구성하는 방법부터 컴포넌트와 컴포저블 테스트, 의존성 모킹, 그리고 채용 팀이 실제로 물어보는 Vue 테스팅 면접 질문에 답하는 방법까지 순서대로 살펴봅니다. > **2026년의 현대적인 Vue 테스팅 스택** > > [Vue 공식 문서](https://vuejs.org/guide/scaling-up/testing.html)가 기본으로 권장하는 조합은 단위 및 컴포넌트 테스트에는 Vitest, 저수준 마운트 API로는 Vue Test Utils, 그리고 종단 간(E2E) 테스트에는 Playwright 또는 Cypress입니다. Vitest는 애플리케이션과 동일한 Vite 설정을 재사용하기 때문에, 테스트가 완전히 같은 변환 파이프라인을 거치며 거의 즉각적인 핫 리로드를 제공합니다. ## Vue 3 프로젝트를 위한 Vitest 구성 Vitest는 Vite 파이프라인을 공유하므로, 하나의 설정 파일이 개발 서버와 테스트 러너를 동시에 구동합니다. 컴포넌트 테스트가 렌더링할 DOM을 갖추려면 `environment` 옵션을 반드시 `jsdom` 또는 `happy-dom`으로 지정해야 합니다. 그리고 `globals: true` 플래그를 켜면 모든 파일에서 별도로 임포트하지 않아도 `describe`, `it`, `expect`를 그대로 사용할 수 있습니다. ```typescript // vitest.config.ts import { defineConfig } from 'vitest/config' import vue from '@vitejs/plugin-vue' import { fileURLToPath } from 'node:url' export default defineConfig({ plugins: [vue()], test: { // jsdom provides document/window so components can mount environment: 'jsdom', // expose describe/it/expect globally globals: true, // run this before each test file (matchers, cleanup) setupFiles: ['./tests/setup.ts'], // only pick up *.spec.ts and *.test.ts files include: ['**/*.{test,spec}.ts'], }, resolve: { alias: { // mirror the @ alias used across the app '@': fileURLToPath(new URL('./src', import.meta.url)), }, }, }) ``` Vitest 3에서는 `vitest`, `@vue/test-utils`, `@vitejs/plugin-vue`, `jsdom`을 설치하고 나면 위 설정이 별도 조정 없이 그대로 동작합니다. 이 별칭이 애플리케이션 설정을 그대로 반영하기 때문에, `@/composables/useCart` 같은 임포트 경로가 테스트와 운영 환경에서 동일하게 해석됩니다. ## Vue Test Utils로 컴포넌트 마운트하기 Vue Test Utils는 두 개의 진입점을 제공합니다. 전체 컴포넌트 트리를 렌더링하는 `mount`와, 자식 컴포넌트를 스텁으로 대체하는 `shallowMount`입니다. 대부분의 단위 테스트에서는 실제 렌더링 동작을 검증하는 `mount`가 더 낫습니다. 반환된 `wrapper`는 렌더링 결과를 검증할 수 있도록 `find`, `get`, `text` 같은 조회 헬퍼를 제공합니다. ```typescript // PriceTag.spec.ts import { describe, it, expect } from 'vitest' import { mount } from '@vue/test-utils' import PriceTag from '@/components/PriceTag.vue' describe('PriceTag', () => { it('formats the amount as currency', () => { const wrapper = mount(PriceTag, { // props are passed through the mounting options props: { amount: 4200, currency: 'EUR' }, }) // get() throws if the selector is missing, unlike find() expect(wrapper.get('[data-test="price"]').text()).toBe('€42.00') }) it('applies a discount class when on sale', () => { const wrapper = mount(PriceTag, { props: { amount: 4200, currency: 'EUR', onSale: true }, }) // classes() returns the array of applied CSS classes expect(wrapper.get('[data-test="price"]').classes()).toContain('price--sale') }) }) ``` CSS 클래스 대신 `data-test` 속성으로 요소를 지정하면 테스트가 훨씬 견고해집니다. `.price--sale`의 스타일을 바꿔도 선택자는 깨지지 않고, 오직 실제 동작이 바뀔 때만 테스트가 반응합니다. 이러한 분리는 유지보수 가능한 Vue 테스트에서 반복적으로 등장하는 원칙입니다. ## 사용자 이벤트와 발생 이벤트 테스트하기 상호작용하는 컴포넌트는 클릭이나 입력 이후 무슨 일이 벌어지는지 검증해야 합니다. Vue Test Utils의 `trigger`는 프로미스를 반환하며, 이를 `await`하면 Vue의 반응성 큐가 비워져 DOM이 갱신 결과를 반영하게 됩니다. `emitted` 헬퍼는 컴포넌트가 발생시킨 모든 커스텀 이벤트를 기록하며, 이것이 부모-자식 간 계약을 검증하는 방법입니다. ```typescript // SearchBar.spec.ts import { describe, it, expect } from 'vitest' import { mount } from '@vue/test-utils' import SearchBar from '@/components/SearchBar.vue' describe('SearchBar', () => { it('emits search with the typed query', async () => { const wrapper = mount(SearchBar) // setValue writes to the input and fires an input event await wrapper.get('input').setValue('vitest') // awaiting trigger flushes the reactivity queue before assertions await wrapper.get('[data-test="submit"]').trigger('click') // emitted() returns a record of every event and its payloads const events = wrapper.emitted('search') expect(events).toHaveLength(1) // events[0] is the first emission, [0] its first argument expect(events![0][0]).toBe('vitest') }) }) ``` > **DOM 갱신을 항상 await하세요** > > Vue는 반응성 갱신을 비동기로 일괄 처리합니다. `trigger`나 `setValue` 호출을 `await`하는 것을 잊는 것이 불안정한(flaky) Vue 테스트의 가장 흔한 원인입니다. DOM이 다시 렌더링되기 전에 검증이 실행되어 오래된 출력을 읽어버리기 때문입니다. 이벤트 헬퍼 바깥에서 상태가 바뀌는 경우에는 `vue`에서 `nextTick`을 임포트한 뒤, 검증 전에 `await nextTick()`을 호출해야 합니다. ## Vue 컴포저블을 독립적으로 테스트하기 반응성 API(`ref`, `computed`, `watch`)만 사용하는 컴포저블은 컴포넌트가 전혀 필요 없습니다. 반응형 상태를 반환하는 순수 함수이기 때문에, 직접 호출한 뒤 `.value`를 검증하는 것으로 충분합니다. 이것이 비즈니스 로직을 가장 빠르고 집중적으로 커버하는 방법이며, [고급 Vue 3 컴포저블](/blog/vue-nuxt/advanced-vue-3-composables-reusable-patterns)에서 다루는 패턴과도 잘 어울립니다. ```typescript // useCart.spec.ts import { describe, it, expect } from 'vitest' import { useCart } from '@/composables/useCart' describe('useCart', () => { it('tracks total price as items change', () => { // composables using only ref/computed run without mounting const { items, total, addItem } = useCart() expect(total.value).toBe(0) addItem({ id: 1, price: 1500, qty: 2 }) // computed total recalculates synchronously on state change expect(total.value).toBe(3000) addItem({ id: 2, price: 500, qty: 1 }) expect(items.value).toHaveLength(2) expect(total.value).toBe(3500) }) }) ``` `onMounted` 같은 라이프사이클 훅에 의존하는 컴포저블은 예외입니다. 이들은 컴포넌트 인스턴스 내부에서 실행되어야 합니다. 흔히 쓰이는 우회 방법은 `setup`에서 컴포저블을 호출하고 그 결과를 노출하는 아주 작은 헬퍼 컴포넌트를 만든 뒤, 그 헬퍼를 Vue Test Utils로 마운트하는 것입니다. ## vi.mock으로 API 호출 모킹하기 실제 네트워크 요청은 테스트를 느리고 비결정적으로 만듭니다. Vitest는 `vi.mock`으로 모듈을 대체하고 `vi.fn`으로 스파이 함수를 생성합니다. 아래 패턴은 `fetch`를 감싸는 모듈을 모킹하여, 테스트 대상 컴포저블이 서버에 전혀 접근하지 않고도 통제된 데이터를 받도록 합니다. ```typescript // useProducts.spec.ts import { describe, it, expect, vi, beforeEach } from 'vitest' import { useProducts } from '@/composables/useProducts' import { getProducts } from '@/api/products' // replace the whole module with mocked exports vi.mock('@/api/products', () => ({ getProducts: vi.fn(), })) describe('useProducts', () => { beforeEach(() => { // reset call history between tests to avoid leakage vi.mocked(getProducts).mockReset() }) it('exposes fetched products and clears loading', async () => { // define what the mocked API returns for this test vi.mocked(getProducts).mockResolvedValue([ { id: 1, name: 'Keyboard' }, ]) const { products, loading, load } = useProducts() await load() expect(getProducts).toHaveBeenCalledOnce() expect(loading.value).toBe(false) expect(products.value[0].name).toBe('Keyboard') }) }) ``` 모듈 경계에서 모킹하면 컴포저블 내부 로직은 그대로 유지하면서, 각 테스트가 오류 경로를 포함한 API 응답을 완전히 통제할 수 있습니다. `mockResolvedValue` 대신 `mockRejectedValue`를 호출하면, 하나의 테스트로 실패가 오류 상태를 설정하고 스피너를 멈추는지까지 검증할 수 있습니다. ## 2026년의 Pinia 스토어 테스트 스토어는 여러 컴포넌트에 걸친 상태를 담고 있으며, 공식 `@pinia/testing` 패키지는 이를 손쉽게 테스트하도록 해줍니다. `createTestingPinia`는 기본적으로 모든 액션을 스텁으로 대체하므로, 컴포넌트 테스트에서 부수 효과를 실행하지 않고도 액션이 디스패치되었는지 검증할 수 있습니다. [Pinia 테스팅 가이드](https://pinia.vuejs.org/cookbook/testing.html)는 컴포넌트 수준 테스트에 이 방식을 권장합니다. ```typescript // CheckoutButton.spec.ts import { describe, it, expect, vi } from 'vitest' import { mount } from '@vue/test-utils' import { createTestingPinia } from '@pinia/testing' import CheckoutButton from '@/components/CheckoutButton.vue' import { useCartStore } from '@/stores/cart' describe('CheckoutButton', () => { it('dispatches checkout on click', async () => { const wrapper = mount(CheckoutButton, { global: { plugins: [ // vi.fn spies on actions instead of executing them createTestingPinia({ createSpy: vi.fn }), ], }, }) const store = useCartStore() await wrapper.get('[data-test="checkout"]').trigger('click') // the action is stubbed, so only the dispatch is asserted expect(store.checkout).toHaveBeenCalledOnce() }) }) ``` 컴포넌트 통합이 아니라 스토어 로직 자체를 테스트하려면 `stubActions: false`를 전달하여 액션이 정상적으로 실행되도록 한 다음, 그 결과 상태를 검증하면 됩니다. 이렇게 액션을 스텁으로 대체한 컴포넌트 테스트와 실제 액션을 실행하는 스토어 테스트를 나누면, 각 테스트 스위트가 단 하나의 책임에 집중하게 됩니다. ## 워치 모드로 테스트 실행하고 커버리지 측정하기 Vitest는 저장된 파일의 영향을 받는 테스트만 다시 실행하는 워치 모드를 기본 제공하여, 개발 중 피드백 루프를 순식간에 단축시킵니다. 커버리지 리포팅은 `@vitest/coverage-v8` 프로바이더를 통해 내장되어 있으며, 지속적 통합(CI) 파이프라인에도 깔끔하게 연동됩니다. 이 파이프라인에서는 실패한 테스트나 커버리지 하락이 병합을 막을 수 있습니다. ```typescript // vitest.config.ts (coverage section) export default defineConfig({ test: { coverage: { // v8 is the fastest provider and needs no instrumentation step provider: 'v8', reporter: ['text', 'html', 'lcov'], // fail CI when a threshold drops below the target thresholds: { statements: 80, branches: 75, functions: 80, }, // exclude generated and config files from the report exclude: ['**/*.config.ts', '**/types/**'], }, }, }) ``` `vitest`만 단독으로 실행하면 로컬에서 워치 모드가 시작되고, `vitest run --coverage`는 CI에 적합한 단일 실행을 수행합니다. 명시적인 임계값을 설정하면 커버리지가 허울뿐인 지표에서 강제되는 품질 게이트로 바뀝니다. 브랜치 커버리지를 75퍼센트 아래로 떨어뜨리는 풀 리퀘스트는 자동으로 실패하게 되고, 이는 기여자가 정상 경로만이 아니라 자신이 추가한 경로까지 테스트하도록 유도합니다. ## 자주 나오는 Vue 테스팅 면접 질문과 답변 테스팅 질문은 후보자가 유지보수성을 어떻게 사고하는지 드러내기 때문에 대부분의 시니어 Vue 면접에서 등장합니다. 아래 답변은 가장 자주 나오는 개념을 다루며, 전체 세트는 [Vue 테스팅 면접 모듈](/technologies/vue-nuxt/interview-questions/vue-testing)에서 반복 연습할 수 있습니다. **`mount` 대신 `shallowMount`는 언제 써야 할까요?** 컴포넌트가 무겁거나 이미 테스트된 자식을 렌더링하고, 테스트가 오직 부모 자신의 로직만 신경 쓸 때 `shallowMount`를 선택합니다. 자식을 스텁으로 대체하면 단위를 격리하고 렌더링 속도를 높일 수 있지만, 컴포넌트 간의 실제 통합은 검증하지 못한다는 대가가 따릅니다. **Vue Test Utils에서 비동기 갱신은 어떻게 처리하나요?** Vue는 반응형 변경을 다음 틱에 적용합니다. `trigger`, `setValue` 같은 이벤트 헬퍼는 DOM이 갱신된 뒤 완료되는 프로미스를 반환하므로, 이를 `await`하는 것으로 충분합니다. 그 헬퍼 바깥에서 바뀐 상태에 대해서는 `await nextTick()`으로 검증 전에 큐를 강제로 비웁니다. **`find`와 `get`의 차이는 무엇인가요?** `find`는 일치하는 요소가 없으면 빈 래퍼를 반환하고 결코 예외를 던지지 않으므로, `expect(wrapper.find('.error').exists()).toBe(false)` 같은 검증에 적합합니다. `get`은 요소가 없으면 즉시 예외를 던지므로, 요소가 존재하리라고 기대되고 그 부재가 진짜 실패에 해당할 때 더 나은 선택입니다. **왜 클래스 선택자보다 `data-test` 속성을 선호하나요?** 클래스 이름은 스타일과 구조에 따라 바뀌므로, 이를 조회하는 테스트는 겉모습만 손봐도 깨집니다. 전용 `data-test` 훅은 테스트 의도를 명시적으로 표현하고 리팩터링에도 살아남아, 거짓 실패(false negative)를 줄여줍니다. **`onMounted`에 의존하는 컴포저블은 어떻게 테스트하나요?** 라이프사이클 훅은 컴포넌트 인스턴스 내부에서만 실행되므로 컴포저블을 직접 호출할 수 없습니다. 표준적인 기법은 `setup`에서 컴포저블을 호출하고 그 반환값을 노출하는 일회용 하니스(harness) 컴포넌트를 만든 뒤, 그 하니스를 Vue Test Utils로 마운트하여 래퍼를 통해 검증하는 것입니다. 이렇게 하면 반응성과 라이프사이클 연결을 온전히 유지하면서도 테스트 대상 로직을 격리할 수 있습니다. 더 많은 개념 및 코딩 질문은 [핵심 Vue.js 면접 질문](/blog/vue-nuxt/essential-vuejs-interview-questions) 가이드에 정리되어 있습니다. ## 결론 2026년의 믿을 수 있는 Vue 테스팅 구성은 결국 몇 가지 의도적인 선택으로 요약됩니다. - `environment: 'jsdom'`과 Vue 플러그인으로 Vitest를 실행하여, 테스트가 애플리케이션과 완전히 동일한 Vite 변환 파이프라인을 공유하게 만듭니다. - 자식 컴포넌트가 무겁거나 다른 곳에서 이미 커버되지 않는 한, `shallowMount`보다 `mount`를 우선합니다. - 타이밍에 의존하는 불안정한 테스트를 없애기 위해, 검증 전에 `trigger`, `setValue`, `nextTick`을 항상 `await`합니다. - 반응성 API만 사용하는 컴포저블은 직접 호출하고 `.value`를 검증하여 테스트합니다. - `vi.mock`으로 모듈 경계에서 모킹하여 네트워크 동작을 결정적으로 만들고 오류 경로까지 테스트 가능하게 합니다. - 컴포넌트 테스트에는 `createTestingPinia({ createSpy: vi.fn })`을, 스토어 로직을 실제로 실행할 때는 `stubActions: false`를 사용합니다. - `data-test` 속성으로 요소를 조회하여, 테스트가 스타일이 아니라 오직 실제 동작 변화에만 깨지도록 합니다. 이 패턴들을 숙달하면 테스팅은 귀찮은 작업이 아니라 설계 도구가 되어, 회귀가 운영 환경에 도달하기 전에 잡아냅니다. 면접에 대비한 실력을 계속 쌓으려면 [Vue와 Nuxt 학습 경로](/technologies/vue-nuxt) 전체를 살펴보시기 바랍니다. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/ko/blog/vue-nuxt/vue-3-testing-vitest-vue-test-utils