[VueJS] VueJS에 Unit Test 적용하기02

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 컴포넌트를 확인하면 된다.

// 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>
//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>
//Todo.vue
<template>
    <li>
        <div class="view">
            <label></label>
            <button class="destroy"></button>
        </div>
    </li>
</template>
<script>
    export default {
        name: 'Todo',
        props: {
            todo: Object,
        }
    }
</script>

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

// 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에 해당하는 데이터가 내려올 경우 그 데이터를 제대로 보여주는가에 대한 테스트이다. 위의 코드와 동일하게 작성되었다면 아래와 같은 초록색 글씨를 볼 수 있다.

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

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

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

//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>
<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 의 테스트 코드는 아래와 같다.

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 를 발생시키느냐.

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);
    });
});

Comments