단위 테스트 삽질기

Todo App에 Unit Test적용하기

전 포스팅에서 Karma+Mocha+Chai를 이용하여 유닛테스트의 환경을 구축하여 간단한 컴포넌트 마운트에 대해서 유닛테스트를 진행했다. 이번에는 Todo APP이라는 주요 기능에 대해서 실제 유닛테스트를 적용시켜보겠다. Todo App의 주요 기능은 다음과 같다.

  • Todo list가 제대로 노출되어야 한다.
  • Todo item이 Todo list에 추가 되어야 한다.
  • Todo item이 삭제가 되어야 한다.
  • Todo item이 수정이 되어야 한다.
  • Todo item이 각각 완료처리를 할 수 있어야 한다.
  • Todo list를 한번에 완료 처리 할 수 있어야 한다.
  • Todo item이 ‘All’, ‘Active’, ‘Completed’ 필터에 맞게 제대로 노출되어야 한다.

위의 체크 사항 순차적으로 돌아가며 유닛테스트에 적용해보도록 하겠다. 적용된 소스는 Github 레파지토리 에서 볼 수 있다.

Todo list 가 제대로 노출되어야 한다.

Todo list의 경우 app의 entry 에서 받은 props의 값을 TodoList 컴포넌트에게 전달해주고, TodoList는 받은 그 데이터를 v-for를 이용하여 Todo 컴포넌트에 바인딩해준다. 그렇다면 list가 제대로 노출되는지를 확인하려면 Todo 컴포넌트를 확인하면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// App.vue
<template>
<div id="app">
<div class="todoapp">
<todo-list
:todos="todoList"
/>
</div>
</div>
</div>
</template>

<script>
import {mapActions, mapGetters} from 'vuex';
import TodoList from './components/TodoList.vue';

export default {
name: 'app',
data() {
return {
currentLocation: window.location.pathname,
todoFilters: ['/all', '/active', '/completed']
}
},
watch: {
currentLocation() {
this.setCurrentLocation(this.currentLocation);
}
},
computed: {
...mapGetters({
todoList: 'getTodoList'
})
},
created() {
this.getTodoList();
this.setCurrentLocation(this.currentLocation);
},
methods: {
...mapActions([
'getTodoList',
'setCurrentLocation'
])
},
components: {
TodoList
}
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//TodoList.vue
<template>
<section class="main">
<ul class="todo-list">
<todo
v-for="todo in todos"
:todo="todo"
:key="todo.key"
/>
</ul>
</section>
</template>
<script>
import Todo from './Todo.vue';
import { mapGetters } from 'vuex';

export default{
name: 'TodoList',
data () {
return {
isEditing: ''
}
},
props: {
todos: Array
},
components: {
Todo
}
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//Todo.vue
<template>
<li>
<div class="view">
<label>{{ todo.todo }}</label>
<button class="destroy"></button>
</div>
</li>
</template>
<script>
export default {
name: 'Todo',
props: {
todo: Object,
}
}
</script>

Todo.vue 컴포넌트의 유닛 테스트 코드는 아래와 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Todo.spec.js
import { mount } from 'avoriaz';
import App from '@/components/Todo.vue';

describe('Todo.vue', () => {
it('Should render correct todo item', () => {
// Mock data
const todo = {
id: "223362f9637d2895",
isDone: true,
todo: "Todo01"
};
// propsData에 Mock data를 등록
const TodoComponent = mount(App, { propsData: { todo }});
const textElement = TodoComponent.find('label')[0];

// propsData의 todo를 제대로 보여주는가?
expect(textElement.text()).to.equal(todo.todo);
});
});

Todo 컴포넌트의 역할은 단순하다. TodoList에서 받은 todo의 데이터를 제대로 랜더링 해주는가가 Todo의 관심사이다. 위의 코드를 보면 todo 라는 변수에 mock data를 넣어주었다. TodoList 컴포넌트로부터 todo에 해당하는 데이터가 내려올 경우 그 데이터를 제대로 보여주는가에 대한 테스트이다. 위의 코드와 동일하게 작성되었다면 아래와 같은 초록색 글씨를 볼 수 있다.

/images/unittest/unittest2.png

이에 해당하는 코드는 github 레파지토리의 feature/get-todo-list 에서 확인할 수 있다.

Todo item이 Todo list에 추가 되어야 한다.

Todo item을 추가하는 기능은 Header컴포넌트의 관심사이다. 기능은 간단하다. Header에 사용자가 입력한 글자가 있으면 해당 아이템을 추가해주고, 글자가 없다면 아무런 액션을 취하지 않는다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
//App.vue
<template>
<div id="app">
<div class="image-wrapper">
<img src="./assets/logo.png">
</div>
<div class="todoapp">
<app-header
@addTodo="addTodo"
/>
<todo-list
:todos="todoList"
/>
</div>
</div>
</template>

<script>
import { mapActions, mapGetters } from 'vuex';

import AppHeader from './components/Header.vue';
import TodoList from './components/TodoList.vue';

export default {
name: 'app',
data() {
return {
currentLocation: window.location.pathname,
todoFilters: ['/all', '/active', '/completed']
}
},
watch: {
currentLocation() {
this.setCurrentLocation(this.currentLocation);
}
},
computed: {
...mapGetters({
todoList: 'getTodoList'
})
},
created() {
this.getTodoList();
this.setCurrentLocation(this.currentLocation);
},
methods: {
addTodo(userValue) {
this.$store.dispatch('addTodo', userValue)
},
changeLocation(currentLocation) {
if (currentLocation.length < 0) return false;
this.currentLocation = currentLocation;

window.history.pushState(
null,
'',
this.currentLocation
)
},
...mapActions([
'getTodoList',
'setCurrentLocation'
])
},
components: {
AppHeader,
TodoList
}
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<template>
<div class="header">
<h1>Todo App</h1>
<input
autofocus="autofocus"
autocomplete="off"
placeholder="What needs to be done?"
class="new-todo"
@keydown.enter="addTodos"
/>
</div>
</template>
<script>
export default {
name: 'header',
data() {
return {
items: ['test1', 'test2']
}
},
methods: {
addTodos(e) {
const textElement = e.target;
const userValue = textElement.value;

if (userValue.length) {
this.$emit('addTodo', userValue);
textElement.value = '';
}
}
}
}
</script>

Header.vue 의 테스트 코드는 아래와 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
require('es6-promise').polyfill();

import { mount } from 'avoriaz';
import chai from 'chai';
import sinonChai from 'sinon-chai';

chai.use(sinonChai);

import Header from '@/components/Header';

describe('Header.vue', () => {
it('Should add a new todo item and after that deleted text field', () => {
// Mock date
const newItem = 'Todo Item01';
const HeaderComponent = mount(Header);
const inputField = HeaderComponent.find('input')[0];

// Set mock data
HeaderComponent.setData({ newItem });

// Set spy for addTodo function
const event = sinon.spy();
HeaderComponent.vm.$on( 'addTodo', event );

inputField.trigger('keydown.enter');

// newItem의 데이터를 추가한 후, 값이 지워지는가
expect(HeaderComponent.data().newItem).to.equal('');
// 부모로부터 내려받은 이벤트를 실행시키는가?
expect(event.calledOnce).to.equal(true);
})
})

위의 코드는 값을 입력한 후, enter 키는 누르면 부모에게 event bus 를 전달하는가와 field가 초기화 되는가에 대한 관심사를 유닛테스트로 검증하고 있다.

Todo item이 삭제가 되어야 한다.

삭제에 대한 기능의 관심사는 2개이다. button을 클릭했을 때, 클릭한 타켓의 primary key를 전달하느냐와 event bus 를 발생시키느냐.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
describe('Delete todo item function', () => {
// Mock data for deleted target
const todo = {
id: "223362f9637d2895",
isDone: true,
todo: "Todo01"
};
const TodoComponent = mount(App, { propsData: { todo }});
const deleteButtonElement = TodoComponent.find('.destroy')[0];

it('Should deleted todo item', () => {
TodoComponent.vm.$on( 'deleteTodo', (todoId) => {
expect(todoId).to.equal(todo.id);
});
});
it('Should occur event bus', () => {
const event = sinon.spy();
TodoComponent.vm.$on( 'deleteTodo', event );
deleteButtonElement.trigger('click');

expect(event.calledOnce).to.equal(true);
});
});
현재 이커머스회사에서 frontend 개발자로 업무를 진행하고 있는 Martin 입니다. 글을 읽으시고 궁금한 점은 댓글 혹은 메일(hoons0131@gmail.com)로 연락해주시면 빠른 회신 드리도록 하겠습니다. 이 외에도 네트워킹에 대해서는 언제나 환영입니다.:Martin(https://github.com/martinYounghoonKim
Javascript Pattern
단위 테스트 삽질기