본문 바로가기
Vue.js

[Vue Test Utils] Vue 테스트 코드 작성 방법2️⃣- jest에 라이브러리 설정하고 사용하기

by devebucks 2021. 8. 21.
728x90

개요

app을 빌드하고 실행하면, vue 인스턴스에 라이브러리들이 import되어서 빌드되면서 라이브러리의 필요한 기능을 사용할 수 있었습니다. 

jest에서는 컴포넌트를 mount해서 실행합니다. jest가 컴포넌트를 실행하다 보니, 컴포넌트가 의존하는 라이브러리의 경로를 못 찾거나 라이브러리의 기능을 온전히 실행하지 못하는 경우가 많습니다. 그래서 test코드나 jest 설정으로 의존하는 라이브러리를 설정하거나 테스트하는 컴포넌트에 주입해줘야 jest로 제대로 기능을 테스트할 수 있습니다.

 

팀에서는 제가 최초로 웹 테스트 방법을 개척하고 있기 때문에 기술문서를 많이 찾아보면서 문제를 해결해 나갔습니다.


jest에 컴포넌트에서 사용하는 라이브러리에 맞춤 설정하기

⚡️ Vuetify를 사용할 경우 설정

jest 실행 환경 전에 vuetify를 vue 인스턴스에 추가하는 js파일 jest 설정에 추가

1. package.json에 jest 설정에 다음 옵션 추가

// 📄package.json
{
    "name": "프로젝트 명",
    "jest": {  // 👈 jest 설정
        // ...
        "setupFilesAfterEnv": [   
            "<rootDir>/jest-setup.js" // 👈vuetify를 Vue 인스턴스에 넣는 js 파일
        ]
        // ...
    }
},

2. jest-setup.js 파일 작성

package.json과 같은 경로에 위치시켰습니다.

// 📄 jest-setup.js
import Vue from 'vue';
import Vuetify from 'vuetify';
Vue.use(Vuetify);

3. login.test.js 테스트 코드 작성

jest 가이드 문서에서는 디렉토리는 보통 테스트할 컴포넌트와 동일한 경로에 두는 것을 추천함.

컴포넌트 mount, shallowMount할 때, vuetify 생성자 추가하기(참고: vuetify Unit Testing)

// 📄src/Modules/Login/login.test.js
//..
import Vuetify from 'vuetify'; //👈

describe('로그인 테스트', () => {
  let vuetify; //👈
  beforeEach(() => {
    vuetify = new Vuetify(); //👈 
  });
  
  it('로그인 요청 성공 테스트', async () => {
    const shallowWrapper = shallowMount(BaseHeader, { router, vuetify });  //👈
   }
}

 

⚡ 테스트 코드에서 Vue Router 사용해서 테스트하는 방법

참고: Vue 테스팅 핸드북

주의할 점

- 전역으로 Vue router를 설치하지 않는다. createLocalVue() 사용해서 vue-router를 주입합니다.

 

 vuex의 state, actions, mutations, getters 등을 사용하는 컴포넌트  테스트 방법

참고 : jest mock function

참고 : Vue Test Utils-Using with Vuex

import Vuex from 'vuex';  // 👈

describe('로그인 테스트', () => {
  const localVue = createLocalVue(); // 👈 해당 테스트에만 vuex를 사용하기 위해서 임시vue 인스턴스에 vuex 의존성을 주입
  localVue.use(Vuex);  
  const store = new Vuex.Store({  // 👈 store 객체 생성
    state: {
      user: {
        gUserInfo: {}
      }
    },
     actions: {
       fetchUserInfo: jest.fn()  // 👈 jest mock function 
     }
  });
  const shallowWrapper = shallowMount(BaseHeader, { localVue, store, router, vuetify }); // 👈 임시 vue 인스턴스와 store 주입한 wrapper 객체 생성
}

 


어려웠던 점과 해결 방법 😩😀

1. 😩 Node 환경에서 import 문법 지원이 안 되는 문제

에러 메시지

({"Object.<anonymous>":function(module,exports,require,__dirname,__filename,jest){import { mount, createLocalVue } from '@vue/test-utils';
SyntaxError: Cannot use import statement outside a module

      at Runtime.createScriptFromCode (node_modules/jest-runtime/build/index.js:1479:14)

원인

- test 코드는 javascript가 Node.js 플랫폼에서 실행됩니다. commonjs 모듈을 사용하는 node.js가 import를 인식하지 못합니다.

 

해결방법

1. npm i -D babel-jest vue-jest 

2. babel.config.js 설정

module.exports = {
    // ...
    env: {
        test: {
            plugins: ['transform-es2015-modules-commonjs']
        }
    }
};

3. package.json에 jest 설정

js와 vue 확장자 파일을 jest 테스트 가능한 코드르 변환 설정

// package.json
{
    "name": "rms-fe",
    // ...
    "jest": {                  //👈 jest 설정
        "moduleFileExtensions": [
            "js",
            "vue"
        ],
        "transform": {  //👈js와 vue 파일을 jest 테스트 가능한 코드로 변환 설정
            "^.+\\.js$": "babel-jest",
            ".*\\.(vue)$": "vue-jest"
        }
    },
    // ...  
}

이 설정을 해서, Jest는 vue 파일을 jest가 이해할 수 있도록 transform 할 수 있고, import 문법을 사용할 수 있게 됩니다.

이 설정을 통해서 test.js 파일 안에서 import 문법을 사용해서 module을 가져올 수 있게 되었습니다.

참고 : https://ui.toast.com/weekly-pick/ko_20180822

 

 

2. 😩 jest가 '@' 경로 alias를 인식하지 못하는 문제 발생

에러 메시지

Cannot find module '@/Modules/LineChart' from 'src/Modules/AdminSetting/Chart/components/chart.vue'

원인

@ 경로 alias를 jest가 인식하지 못해서 발생함.

 

해결 방법

package.json에 jest 설정

- webpack 설정으로 alias를 설정한 것처럼, jest도 alias설정을 해줌

"jest": {
  // ...
  "moduleNameMapper": {
     "^@/(.*)$": "<rootDir>/src/$1"   //👈 webpack 설정으로 alias를 설정한 것처럼, jest도 alias설정을 해줌
  },
  // ...
}


3. 😩 web app의 화면 요소들을 테스트

jest의 기본 테스트 환경은 Node.js입니다. 브라우저 환경처럼 렌더링을 거쳐서 화면 요소를 DOM 처럼 가지고 있지 않습니다.

에러 메시지를 보면, window 객체부터 못 찾아서 에러가 발생한 것을 알 수 있습니다.

에러 메시지

[vue-test-utils]: window is undefined, vue-test-utils needs to be run in a browser environment. You can run the tests in node using jsdom See https://vue-test-utils.vuejs.org/guides/#browser-environment for more details.

원인

jest의 테스트 코드 파일이 node 환경으로 실행되기 때문에 화면 요소 객체를 만들지 못 함.

 

해결 방법

참고 : https://jestjs.io/docs/configuration#testenvironment-string

브라우저의 실행 환경과 같이 만들어 줘야 합니다.

Jest 27버전 문서를 확인해 보니, 주석으로 test.js파일에 브라우저와 같은 환경을 설정할 수 있다고 나와있었습니다.

파일 가장 상단에 아래 주석을 입력해주면 됩니다.

 

설정 파일 : 컴포넌트를 마운트해서 사용할 test.js 파일 상단에 아래 주석을 작성.

/**
 * @jest-environment jsdom
 */

 

 

4. 😩 jest에서 vuetify 태그<v-container> | <v-btn> ...을 인식할 수 있게 하기

에러메시지

원인

테스트 코드에서 mount한 컴포넌트에 vuetify의 테그가 있으면, jest는 이 태그들을 인식할 수 없습니다.

 

해결 방법

1. package.json 경로에 jest-setup.js파일 만들기

프로젝트 홈 경로/jest-setup.js

import Vue from 'vue';
import Vuetify from 'vuetify';
Vue.use(Vuetify);

2. package.json에 jest 'setupFilesAfterEnv' 설정 추가하기

프로젝트 홈 경로/package.json

{
    "name": "rms-fe",
    "jest": {
        "setupFilesAfterEnv": [    // 👈 이 부분 추가
            "<rootDir>/jest-setup.js"   //👈 <rootDir> 그대로 사용하기 <rootDir>은 프로젝트 홈 경로를 지칭함.
        ]
    },
    // ...
    
  }

Vuetify의 v-container 태그를 사용하는 login 페이지 테스트 성공

 

 

5. 😩 jest가 Vuelidate 라이브러리를 인식 못 함.

Vuelidate를 통해서 required 검증을 하는 input을 테스트 하는데, 컴포넌트에서 vuelidate의 객체인 $v를 인식하지 못했음.

 

원인

jest는 vuelidate의 $v객체를 모름.

 

해결 방법

test.js 테스트 파일에 Vue 인스턴스에 vuelidate라이브러리를 주입해주는 방식으로 해결함.

 

6. 😩 프로젝트에서 사용하는 router를 test 코드에 가져다 사용하기

TypeError: routes.forEach is not a function

원인

테스트 코드에 import한 @/router/index.js가 export default로 router 객체를 VueRouter 생성자의 매개변수로 넣는 것이 문제였음.

new VueRouter()의 매개변수는 Iterable 타입이어야 함.(=배열)

 

해결 방법

객체를 반환하는 파일을 import 했음. router/index.js에서 router 정보를 가지는 routes 배열을 넣어줘야 함.

@/router/index.js

const routes = [
	// ...
    {
        path: '/group',
        name: 'GROUP',
        component: () => import('@/pages/Group.vue')
    },
    {
        path: '/usersetting',
        name: 'user-setting',
        component: () => import('@/pages/UserSetting.vue')
    }
];

const router = new VueRouter({
    mode: 'history',
    scrollBehavior() {
        return { x: 0, y: 0 };
    },
    routes
});

export default router;

export { routes }; // 👈 배열형의 routes 정보 배열을 export함.

login.test.js

/**
 * @jest-environment jsdom
 */
import Vue from 'vue';
// ...
import { routes } from '@/router/index.js';  //👈 배열 형태를 import 함.

const localVue = createLocalVue();
localVue.use(VueRouter);

describe('로그인 테스트', () => {
   //...

    it('로그인 요청 성공 테스트', async () => {
        const router = new VueRouter({ routes });  
        const wrapper = mount(Login, { localVue, router });
        const loginBtn = wrapper.find('[element-test="login-btn"]');
        const id = wrapper.find('[element-test="id-input"]');
        const password = wrapper.find('[element-test="password-input"]');
        id.setValue('admin');
        password.setValue('admin');
        // login 컴포넌트의 data property에 값이 들어가는 지 확인
        await loginBtn.trigger('click');
        // 로그인 api 응답 success 확인

        //라우터가 intro로 가는 지 확인
        expect(wrapper.vm.$route.name).toBe('FUEL CELL');
    });
});

 

 

7. 😩 jest가 export를 인식 못 함.

에러메시지

SyntaxError: Unexpected token 'export'

원인

node_modules의 코드까지 jest가 테스트하려 함.

 

해결 방법

package.json에 jest 설정 추가

{
  "jest": {
    //...
    "transformIgnorePatterns": [ "<roodDir>/node_modules/(?!@swish)" ]
    // ...
  }
}

실무 로그인 테스트 코드

프로젝트 홈 경로/src/Modules/Login/index.vue

<template>
    <v-container fluid class="login d-flex justify-center align-center">
        <v-card width="320" height="500" class="login_box d-flex flex-column justify-end align-center px-10 pt-15">
            <img src="" alt="" />
            <p class="text-h4 font-weight-bold pt-10 mb-16">Login</p>
            <v-text-field v-model="sId" :error-messages="sIdError" placeholder="ID" dense outlined element-test="id-input"></v-text-field>
            <v-text-field
                v-model="sPassword"
                type="password"
                class="mx-0"
                :error-messages="sPasswordError"
                dense
                outlined
                placeholder="PASSWORD"
                element-test="password-input"
            ></v-text-field>
            <v-container class="mb-9 pa-0">
                <v-btn block rounded color="#741eec" class="" @click="loginSubmit()" element-test="login-btn">Login</v-btn>
                <p class="text-center red--text" v-if="sSubmitStatus === ''"><br /></p>
                <p class="text-center red--text" v-if="sSubmitStatus === 'OK'">Thanks for your submission!</p>
                <p element-test="submit-status-message" class="text-center red--text" v-if="sSubmitStatus === 'ERROR'">Please fill the form correctly.</p>
                <p class="text-center red--text" v-if="sSubmitStatus === 'PENDING'">Sending...</p>
            </v-container>

            <p class="text-caption grey--text">Copyright ⓒ2021 MACHBASE</p>
        </v-card>
    </v-container>
</template>
<script>
import { required } from 'vuelidate/lib/validators';
export default {
    name: 'login',
    validations: {
        sId: { required },
        sPassword: { required }
    },
    computed: {
        sIdError() {
            let errors = [];
            if (!this.$v.sId.$dirty) return errors;
            if (!this.$v.sId.required) {
                errors.push('input id.');
            }
            return errors;
        },
        sPasswordError() {
            let errors = [];
            if (!this.$v.sPassword.$dirty) return errors;
            if (!this.$v.sPassword.required) {
                errors.push('input password.');
            }
            return errors;
        }
    },
    methods: {
        loginSubmit() {
            this.$v.$touch();
            if (this.$v.$dirty === true) {
                this.sSubmitStatus = 'ERROR';
                return;
            } else {
                this.sSubmitStatus = 'PENDING';
                setTimeout(() => {
                    this.sSubmitStatus = 'OK';
                    this.$router.push('/intro');
                }, 500);
            }
        }
    },
    data() {
        return {
            sId: '',
            sPassword: '',
            sSubmitStatus: ''
        };
    }
};
</script>
<style lang="scss" scoped>
.login {
    background-color: $BACKGROUND_GRAY;
    height: 100vh;
}

.v-input {
    width: 100%;
}
</style>

프로젝트 홈 경로/src/Modules/Login/index.test.js

/**
 * @jest-environment jsdom
 */
import Vue from 'vue';
import { mount } from '@vue/test-utils';
import Login from './index.vue';
import Vuelidate from 'vuelidate';  // 👈 vuelidate를 사용할 경우 추가 
Vue.use(Vuelidate);                 // 👈 vuelidate를 사용할 경우 추가
describe('로그인 테스트', () => {
    it('element 존재 테스트', () => {
        const wrapper = mount(Login);
        //아이디와 패스워드 입력창 존재
        const idInput = wrapper.find('[element-test="id-input"]');
        const pwInput = wrapper.find('[element-test="password-input"]');
        const loginBtn = wrapper.find('[element-test="login-btn"]');
        expect(idInput.exists()).toBe(true);
        expect(pwInput.exists()).toBe(true);
        expect(loginBtn.exists()).toBe(true);
    });

    it('로그인 id와 pw null submit 요청했을 때, submit 안 되는 지 테스트', async () => {
        const wrapper = mount(Login);
        const loginBtn = wrapper.find('[element-test="login-btn"]');
        const id = wrapper.find('[element-test="id-input"]');
        const password = wrapper.find('[element-test="password-input"]');
        // id.setValue('123'); // 👈 값 대입
        expect(id.element.value).toBe('');
        expect(password.element.value).toBe('');
        await loginBtn.trigger('click');
        const submitMessage = wrapper.find('[element-test="submit-status-message"]');
        expect(submitMessage.exists()).toBe(true);
        expect(submitMessage.text()).toContain('Please fill the form correctly.');
    });
});

 

728x90

댓글