Vue 2.7 + vitest 사용시 주의점

3 minute read

1. vitest 설정

vitest.config.ts는 vite.confit.ts 파일을 ‘확장’하지 않는다. 덮어쓴다. 따라서 vitest 실행에 필수적인 설정은 vitest.config.ts에 모두 설정해놔야 한다. 예를 들어, 아래 코드처럼 vue2 plugin 설정을 하지 않으면 vue3로 test를 실행한다. 따라서 vue syntax 오류가 발생한다

import vue2 from '@vitejs/plugin-vue2';
import { defineConfig } from 'vitest/config';

export default defineConfig({
  plugins: [vue2()],
  test: {
    globals: true,
    environment: 'jsdom',
    alias: [{ find: /^vue$/, replacement: 'vue/dist/vue.runtime.common.js' }],
  },
});

2. vue-test-utils 버전

vue-test-utils를 사용한다면 버전은 ^1.3.0으로 설치해야 한다. 최신 버전은 vue3 전용이다

3. vue-test-utils와 composite API

vue-test-utils은 composite API를 사용한 SFC를 vue3component로 인식한다. 그래서 모든 테스트 코드가 공식 문서대로 동작한다고 보장할 수 없다. 예를 들어 아래와 같은 테스트 코드를 작성해보자.

const wrapper = mount(component);
const buttonA = wrapper.findComponent(`.ButtonA`);
await buttonA.trigger('click');

buttonA를 클릭할 때 호출되는 함수(v-on:click 혹은 watchEffect)는 호출될까? 호출되지 않는다. 그러니 click 이벤트를 테스트 할 수 없다.

https://github.com/vuejs/vue-test-utils/issues/1983 의 논의를 보면 앞으로 해결될 것 같지도 않다.

4. 그러면 테스트는 어떻게 해?

나는 테스트 코드에서 store의 값을 직접 변경하는 방식으로 테스트한다. 어차피 대부분의 사용자 인터렉션은 store의 값을 변경하기 위한 작업이다. store 값을 변경하는 비즈니스 로직을 별도 함수로 분리하고, 테스트 코드에서 해당 함수를 호출한다. 요새 대세인 pinia를 store로 많이들 사용할텐데, 다행히 pinia는 vue 2.7을 지원한다.

const component = mount(component);

const store = useStore();
store.data = 'test'; // pinia store의 갑 변경

await wrapper.vm.$nextTick(); // vue가 새로 렌더링 하기를 기다린다

expect(component.html()).toContain('test');

만약 store에 초기값이 있는 경우라면 createTestingPinia가 제공하는 initState에 초기값을 명시하면를 된다.

const wrapper = mount(method, {
  localVue,
  pinia: createTestingPinia({
    initialState: {
      productDetail: { pageType: 'UPDATE' }, // 상품수정 모드
      salesInfo: { sellMethodCode: SELL_METHOD_CODE.USED }, // 중고판매
    },
  }),
});

참고로 computed로 만든 getter는 pinia에서 수정을 못하므로 위 방법을 사용할 수 밖에 없다.

5. 그러면 사용자 인터렉션은 테스트 못하나?

그건 e2e 테스트를 도입 하시라(나도 안 해봄). Vue3가 vitest와 puppeteer를 사용해서 짠 e2e 테스트 코드가 있으니 참고하면 될 것 같다. https://github.com/vuejs/core/blob/main/packages/vue/__tests__/e2e/e2eUtils.ts

6.Type

mountshallowMount를 하면 이런 타입 오류가 뜬다.

import { mount } from '@vue/test-utils';
import WhatComponent from '@/WhatComponent.vue'

const wrapper = mount(WhatComponent);
TS2321: Excessive stack depth comparing types 'ComponentPublicInstance {     | WritableComputedOptions > = {}, M extends MethodOptions = {}, Mixin extends ComponentOptionsMixin = ComponentOptionsMixin, Extends extends ComponentOptionsMixin = ComponentOptionsMixin, Emits extends EmitsOpti...' and 'Vue  , Record , never, never, (event: string, ...args: any[]) => Vue  , Record , never, never, ...>>'.
TS2321: Excessive stack depth comparing types 'DefineComponent{     | WritableComputedOptions > = {}, M extends MethodOptions = {}, Mixin extends ComponentOptionsMixin = ComponentOptionsMixin, Extends extends ComponentOptionsMixin = ComponentOptionsMixin, Emits extends EmitsOptions = {}, EmitsNa...' and 'VueClass   , Record , never, never, (event: string, ...args: any[]) => Vue  , Record , never, never, ...>>>'.
TS2321: Excessive stack depth comparing types 'Vue3Instance{}, Readonly{     | WritableComputedOptions > = {}, M extends MethodOptions = {}, Mixin extends ComponentOptionsMixin = ComponentOptionsMixin, Extends extends ComponentOptionsMixin = ComponentOptionsMixin, Emits extends EmitsOptions = {...' and 'Vue  , Record , never, never, (event: string, ...args: any[]) => Vue  , Record , never, never, ...>>'.
TS2769: No overload matches this call.   The last overload gave the following error.     Argument of type 'DefineComponent<{ <RawBindings, D = {}, C extends Record<string, ComputedGetter<any> | WritableComputedOptions<any>> = {}, M extends MethodOptions = {}, Mixin extends ComponentOptionsMixin = ComponentOptionsMixin, Extends extends ComponentOptionsMixin = ComponentOptionsMixin, Emits extends EmitsOptions = {}, EmitsNa...' is not assignable to parameter of type 'ExtendedVue<Vue<Record<string, any>, Record<string, any>, never, never, (event: string, ...args: any[]) => Vue<Record<string, any>, Record<string, any>, never, never, ...>>, ... 6 more ..., ComponentOptionsMixin>'.       Type 'ComponentPublicInstanceConstructor<Vue3Instance<{}, Readonly<{ <RawBindings, D = {}, C extends Record<string, ComputedGetter<any> | WritableComputedOptions<any>> = {}, M extends MethodOptions = {}, Mixin extends ComponentOptionsMixin = ComponentOptionsMixin, Extends extends ComponentOptionsMixin = ComponentOptionsMi...' is missing the following properties from type 'VueConstructor<ExtractComputedReturns<{}> & Record<string, any> & Vue<Record<string, any>, Record<string, any>, never, never, (event: string, ...args: any[]) => Vue<...>> & ShallowUnwrapRef<...> & Vue<...>>': extend, nextTick, set, delete, and 10 more. 

vue-test-utilsVue2.7을 제대로 지원하지 않기 때문이다. 이 문제는 앞으로라고 해결될 것 같지 않다. 하지만 우회책은 있다.

import { mount } from '@vue/test-utils';
import WhatComponent from '@/WhatComponent.vue'

const component: Component = WhatComponent; // import한 component의 type을 제한

const wrapper = mount(component);

위와 같이 import한 component의 type을 제한하면 된다.

7. export default

컴포넌트를 export default 하면 이런 오류가 발생한다.

vitest-export-default-error

vitest가 vue2.7의 컴포지트 api를 제대로 지원하지 않아 발생하는 오류다.

<script setup>/* vue setup code */</script> 내부에 export default를 할 수 없으므로, 하기 코드와 같이 <script>export default {}</script> 태그를 하나 더 만들어서 export default를 빼주면 해결된다.

<script lang="ts">
import BaseDropdown from '../BaseDropdown.vue';

export default {
  inheritAttrs: false,
  components: { BaseDropdown },
};
</script>

20230426

Tags:

Categories:

Updated:

Leave a comment