뷰 테스트 유틸 라이브러리를 이용하여 간단한 테스트 코드를 작성해보겠다.
아래의 코드를 가지고 npm t
를 실행한다.
<!-- HelloWorld.vue -->
<template>
<div>Hello {{ message }}</div>
</template>
<script>
export default {
data() {
return {
message: 'Vue!'
}
}
}
</script>
// helloworld.test.js
import Vue from 'vue';
import HelloWorld from './HelloWorld.vue';
test('HelloWorld Component', () => {
const cmp = new Vue(HelloWorld).$mount();
expect(cmp.message).toBe('Vue!');
});
코드가 실행되면 HelloWorld 컴포넌트가 정상적으로 로딩되면서 테스트가 통과된다.
TIP
HelloWorld
컴포넌트 파일 경로 주의
앞에서 살펴본 테스트 코드의 번거러운 점은 컴포넌트의 그능을 테스트하기 위해 매번 뷰 인스턴스를 생성하고 $mount()
API를 호출해야 한다는 점이다.
다행히도 뷰 테스트 유틸 라이브러리에서 이 반복 작업을 편하게 해주는 API를 제공한다.
바로 shallowMount()
와 mount()
이다.
먼저 shallowMount()
API를 사용해보자.
앞에서 살펴본 코드에 shallowMount()
API를 적용해보면
// helloworld.test.js
import { shallowMount } from '@vue/test-utils';
import HelloWorld from './HelloWorld.vue';
test('HelloWorld Component', () => {
const wrapper = shallowMount(HelloWorld);
expect(wrapper.vm.message).toBe('Vue!');
});
뷰 테스트 라이브러리에서 shallowMount()
API를 불러온 후 HelloWorld 컴포넌트의 message
속성을 테스트 하였다.
앞서 살펴본 코드와 차이나는 부분은 아래와 같다.
// 뷰 테스트 유틸 API를 사용하지 않은 경우
const cmp = new Vue(HelloWorld).$mount();
// 뷰 테스트 유틸 API를 사용한 경우
const wrapper = shallowMount(HelloWorld);
new Vue().$mount()
로 접근하던 형식을 mount()
로 간소화 하였다.
앞에서 살펴본 shallowMount()
API 이외에 mount()
API도 테스트 할 때 자주 사용된다.
shallowMount()
는 지정된 컴포넌트의 내용만 테스트할 때 사용하고, mount()
는 해당 컴포넌트에 등록된 하위 컴포넌트의 내용들까지 확인할 때 사용한다.
이번 튜토리얼에서는 간단한 Todo App을 만들면서 각 컴포넌트의 유닛 테스트(Unit Test)를 작성해보겠다.
커모넌트를 구현하고 핻아 컴포넌트의 테스트 코드를 작성하는 순서로 작업한다.
npm install -g @vue/cli
vue create todo-app-test
env
옵션 속성에 jest: true
추가module.exports = {
root: true,
env: {
node: true,
jest: true, // jest api들을 사용할 때 에러 표시가 나지 않게 해준다.
},
// ...
}
@types/jest
라이브러리 설치@types/jest
라이브러리는 jest api들의 자동완성을 제공한다.npm install @types/jest -D
testMatch
설정 추가module.exports = {
preset: "@vue/cli-plugin-unit-jest",
testMatch: ["**/src/**/*.(test|spec).js"], // src 폴더 내의 파일 이름에 spec이나 test가 포함돼 있다면 테스트를 수행한다.
}
<!-- src/App.vue -->
<template>
<div>
<h1>Todo App</h1>
</div>
</template>
// src/App.test.js
import { shallowMount } from "@vue/test-utils";
import App from "./App.vue";
describe("App", () => {
it("renders title", () => {
const wrapper = shallowMount(App);
expect(wrapper.find(".h1").text()).toMatch("Todo App");
});
});
이제 테스트 코드를 실행해 주면 된다.
npm run test:unit
터미널 창에 아래와 같이 출력 된다면 프로젝트 셋업이 정상적으로 완료된 것이다.
> vue-cli-service test:unit
PASS src/App.test.js
App
✓ renders title (21ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 1.403s
Ran all test suites.
TIP
// package.json { //... "scripts": { // ... "test:unit": "vue-cli-service test:unit --watchAll" } }
package.json에
test:unit
스크립트에--watchAll
옵션을 추가해주자.
테스트가 추가되거나 수정되면 자동으로 다시 실행시켜준다.
프로젝트 준비가 끝났으니 Todo App을 구현해보겠다.
우리가 구현해야 되는 기능들은 아래와 같으며 차례대로 구현한다.
할 일 추가하기 구현 순서는 다음과 같다.
아래와 빨간 박스로 표시된 부분의 UI를 구현한다.
<!-- src/App.vue -->
<template>
<div>
<h1>Todo App</h1>
<div>
<label for="todo-control">할 일 작성</labe>
<div>
<input
:id="todo-control"
type= "text"
placeholder="할 일을 작성해주세요"
/>
<button type="button">추가하기</button>
</div>
</div>
</div>
</template>
UI 구현 코드에서 작성해야 되는 테스트 코드는 다음과 같다.
한 가지 알아두셔야 할 것은 테스트 코드는 작성하는 사람에 따라 얼마든지 변경될 수 있다.
테스트 코드를 얼마나 세세하게 작성하냐에 따라서 코드의 안정성이 달라진다.
즉 테스트 코그를 작성한다고 해서 모든 에러를 방지 할 수없는게 아니다.
그러므로 조금이라도 더 세세하게 테스트를 작성해보는걸 권장한다.
// src/App.test.vue
import { shallowMount } from "@vue/test-utils";
import App from "./App.vue";
describe("App", () => {
it("renders title", () => {
const wrapper = shallowMount(App);
expect(wrapper.find("h1").text()).toBe("Todo App");
});
it("renders label, input", () => {
const wrapper = shallowMount(App);
// '할 일 작성'이라는 텍스트가 화면에 출력된다.
expect(wrapper.find("label").text()).toMatch("할 일 작성");
// 할 일을 작성할 수 있는 'control'창 이 화면에 출력된다.
expect(wrapper.find("input").attrbutes("placeholder")).toMatch("할 일을 작성해주세요");
});
});
it("renders button", () => {
const wrapper = shallowMount(App);
// '추가하기'라는 버튼이 화면에 출력된다.
expect(wrapper.find("button").text()).toMatch("추가하기");
});
막상 테스트 코드를 작성을 하고 보니 한가지 아쉬운 게 있다.
인풋 태그와 레이블이 연결된 지도 확인해보고 싶으므로 테스트를 하나 더 추가해보겠다.
it("connects label and input", () => {
const wrapper = shallowMount(App);
const TODO_CONTROL = 'todo-control';
expect(wrapper.find("label").attrbutes("for")).toMatch(TODO_CONTROL);
expect(wrapper.find("input").attrbutes("id")).toMatch(TODO_CONTROL);
});
이런 식으로 학습하시면서 아쉬운 부분들이 생긴다면 테스트를 추가해보자.
그리고 위의 테스트 코드에서 describe
, it
의 첫 번째 인수를 합쳐보면 "App renders label, input", "App renders button"으로 문장이 만들어지는 것을 볼 수 있다.
"render"는 "App"이 단수이기 때문에 s를 붙여서 "renders"로 작성하는 것이다.
이런 식으로 문법에 맞게 작성해주고 테스트 코드를 작성할때는 항상 말이 되게 작성하는것을 권장한다.
이렇게 작성하게 되면 테스트를 실행했을 때도 아래와 같이 수월하게 읽을 수 있다.
PASS src/App.test.js
App
✓ renders title (21ms)
✓ renders label, input (5ms)
✓ connects label and input (3ms)
그리고 나중에 해당 컴포넌트의 역할을 파악하고 싶을 때 관심사의 분리(sparation of concerns, SoC)를 잘해 놓았다면 테스트 코드만으로도 파악이 가능하다.
이 부분은 튜토리얼 마지막 부분에서 다뤄보겠다.
<!-- src/App.vue -->
<template>
<div>
<h1>Todo App</h1>
<div>
<label for="todo-cotrol">할 일 작성</label>
<div>
<input
id="todo-control"
type="text"
placeholder="할 일을 작성해주세요"
:value="text"
@input="handleInput"
/>
<button type="button">추가하기</button>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
text: "",
}
},
methods() {
handleInput(event) {
this.text = event.target.value;
},
},
};
</script>
v-model을 사용하지 않은 이유는 현재 시점에서 IME 입력(한국어, 일본어, 중국어)에 대해서 한계점이 있기 때문이다.
참고
// src/App.test.js
it("changes input value when listens input event", async () => {
const wrapper = shallowMount(App);
// setValue는 아래 두 코드의 축약 api 이다.
await wrapper.find("input").setValue("아무것도 안하기");
// wrapper.find("input").element.value = "아무것도 안하기";
// wrapper.find("input").trigger("input");
expect(wrapper.vm.text).toMatch("아무것도 안하기");
});
vue-test-utils 라이브러리에선 input이벤트를 trigger시 event.target.value
를 직접적으로 변경할 수 없다.
그래서 input의 value값을 변경한 뒤 input이벤트를 trigger해야 한다.
input이벤트를 trigger하면 handleInput
함수가 실행되고 data
의 text
값이 변경되었는지 테스트한다.
그리고 이벤트 트리거는 비동기로 동작하기 때문에 async, await
문법을 사용하여 실행 순서를 보장해주어야 한다.
<!-- src/App.vue -->
<template>
<div>
<h1>Todo App</h1>
<div>
<label for="todo-control">할 일 작성</label>
<div>
<input
id="todo-control"
type="text"
placeholder="할 일을 작성해주세요"
:value="text"
@input="handleInput"
/>
<button type="button" @click="handleClickAppTodo">추가하기</button>
</div>
</div>
<ul>
<li v-for="todo in todos" :key="todo.id">
{{ todo.text }}
</li>
</ul>
</div>
</template>
<script>
export default {
data() {
return {
text: "",
newId: 0,
todos: [],
}
},
methods: {
handleInput(text) {
this.text = event.text.value;
},
handleClickAddTodo() {
// 할 일 추가
this.todos.push({
id: this.newId,
text: this.text,
});
this.newId += 1;
// 인풋 값 초기화
this.text = "";
},
},
};
</script>
// src/App.test.js
it("adds todo when listens '추가하기' click event", async () => {
const wrapper = shallowMount(App);
wrapper.find("input").setValue("아무것도 안하기");
wait wrapper.find("button").trigger("click");
expect(wrapper.find("li").text()).toContain("아무것도 안하기");
});
인풋 태그에 할 일을 타이핑하고 "추가하기" 버튼이 클릭 됐을 때 타이핑한 할 일이 화면에 출력 되는지 테스트한다.
단위 테스트 케이스 작성을 위해 자주 사용하는 API 목록이다.
테스트 할 컴포넌트의 기능만 테스트하고 하위 컴포넌트와는 분리해주는 테스트 API
import { shallowMount } from '@vue/test-utils';
import Component from './component';
describe('Component', () => {
test('is a Vue instance', () => {
const wrapper = shallowMount(Component);
expect(wrapper.isVueInstance()).toBeTruthy();
})
})
mount a component without rendering its child components
테스트할 컴포넌트의 하위 컴포넌트의 동작까지 함께 테스트 하는 API
// helloworld.test.js
import { mount } from '@vue/test-utils';
import HelloWorld from './HelloWorld.vue';
test('testWorld Component', () => {
const wrapper = mount(HelloWorld);
expect(wrapper.vm.message).toBe('Vue!');
})
스냅샷(Snapshot Testing) 테스팅을 하기 위해 먼저 아래 환경을 구성한다.
npm install --save-dev jest-serializer-vue
// package.json
{
// ...
"jest": {
// ...
// serializer for snapshots
"snapshotSerializers": [
"jest-serializer-vue"
]
}
}
그리고 코드는 다음과 같다.
import { mount } from '@vue/test.utils';
import HelloWorld from '../HelloWorld.vue';
describe('Hello World Component', () => {
test('[Snapshot Testing] renders hello world message', () => {
const { vm } = mount(HelloWorld);
expect(vm.$el).toMatchSnapshot();
})
})
Comfidence to change / Removal of Fear + High Code Quality + Well-documented Code = Deeloper Happiness
뷰 컴포넌트 테스트 코드를 작성할 때 고민할 지점들은 다음과 같다.
테스트 할 필요가 없는 지점들
mount()
API 사용.shallowMount()
API 사용. 특정 기능의 흐름을 잘게 분할해서 테스트 케이스로 작성.