[Web] Vue Test Utils Crash Course 요약

olwooz·2023년 6월 4일
0

Web

목록 보기
2/3

Vue Test Utils (VTU) - Vue.js 컴포넌트 테스트를 쉽게 만들어주는 유틸
Vue Test Utils Crash Course

TodoApp 예제

<template>
  <div></div>
</template>

<script>
export default {
  name: 'TodoApp',

  data() {
    return {
      todos: [
        {
          id: 1,
          text: 'Learn Vue.js 3',
          completed: false
        }
      ]
    }
  }
}
</script>

todo 렌더 테스트

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

test('renders a todo', () => {
  const wrapper = mount(TodoApp)

  const todo = wrapper.get('[data-test="todo"]')

  expect(todo.text()).toBe('Learn Vue.js 3')
})
  1. mount import
    • mount - VTU에서 컴포넌트를 렌더링하는 주요 방법
  2. test 함수에 간단한 테스트 설명을 적어 테스트 선언
  3. mount에 컴포넌트를 전달해 호출 (거의 모든 테스트에서 하게 될 일) → 결과를 wrapper라는 변수에 저장 (convention, mount가 앱을 감싸 테스트를 위한 편리한 메서드들을 제공하기 때문)
  4. expect - 실제 값과 우리가 기대하는 값 매치
    • 위 예제에서는 data-test="todo" 를 가지는 요소를 찾아 text 메서드로 내용을 받아옴, 해당 내용이 'Learn Vue.js 3'일 것이라 기대

테스트 패스시키기

아직 todo가 없기 때문에 위 테스트를 그대로 돌리게 되면 get() 메서드가 반환할 wrapper가 없어서 Unable to get [data-test="todo"] 에러 발생

TodoApp.vue<template>에서 todos array를 렌더하도록 변경하면 테스트 패스 가능

<template>
  <div>
    <div v-for="todo in todos" :key="todo.id" data-test="todo">
      {{ todo.text }}
    </div>
  </div>
</template>

todo 추가 기능 구현

유저가 새 todo를 추가하도록 하기 위해서는 유저가 text를 입력할 form 필요
유저가 form을 제출하면 새로운 todo가 렌더되어야 함 → 이에 대한 테스트 작성

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

test('creates a todo', () => {
  const wrapper = mount(TodoApp)
  expect(wrapper.findAll('[data-test="todo"]')).toHaveLength(1)

  wrapper.get('[data-test="new-todo"]').setValue('New todo')
  wrapper.get('[data-test="form"]').trigger('submit')

  expect(wrapper.findAll('[data-test="todo"]')).toHaveLength(2)
})

테스트 코드 설명

  • mount로 요소를 렌더링하는 것으로 시작
  • “todo 1개가 렌더되었다” 단언
  • <input>을 업데이트하기 위해 setValue 사용
  • <input> 업데이트 후 유저의 form 제출 동작을 시뮬레이션 하기 위해 trigger 사용
  • “todo의 갯수가 1개에서 2개로 늘어났다” 단언

테스트를 패스시키기 위해 TodoApp.vue<form><input> 추가:

<template>
  <div>
    <div v-for="todo in todos" :key="todo.id" data-test="todo">
      {{ todo.text }}
    </div>

    <form data-test="form" @submit.prevent="createTodo">
      <input data-test="new-todo" v-model="newTodo" />
    </form>
  </div>
</template>

<script>
export default {
  name: 'TodoApp',

  data() {
    return {
      newTodo: '',
      todos: [
        {
          id: 1,
          text: 'Learn Vue.js 3',
          completed: false
        }
      ]
    }
  },

  methods: {
    createTodo() {
      this.todos.push({
        id: 2,
        text: this.newTodo,
        completed: false
      })
    }
  }
}
</script>

정상 작동할 것 같지만 테스트는 실패하게 됨:

expect(received).toHaveLength(expected)

    Expected length: 2
    Received length: 1
    Received array:  [{"element": <div data-test="todo">Learn Vue.js 3</div>}]

todo의 갯수가 증가하지 않았음

  • Jest는 테스트를 동기적으로 실행하기 때문에 마지막 함수가 호출되는 순간 테스트를 종료하기 때문에 발생하는 문제

Vue는 DOM을 비동기적으로 업데이트하기 때문에 테스트를 async로 선언하고 DOM이 변경되는 메서드를 await 해야 함 (e.g. trigger, setValue 등)

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

test('creates a todo', async () => {
  const wrapper = mount(TodoApp)

  await wrapper.get('[data-test="new-todo"]').setValue('New todo')
  await wrapper.get('[data-test="form"]').trigger('submit')

  expect(wrapper.findAll('[data-test="todo"]')).toHaveLength(2)
})

todo 완료 기능 구현

유저가 체크박스로 todo 아이템이 완료되었는지 표시할 수 있게 해주는 기능 구현

실패하는 테스트로 시작:

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

test('completes a todo', async () => {
  const wrapper = mount(TodoApp)

  await wrapper.get('[data-test="todo-checkbox"]').setValue(true)

  expect(wrapper.get('[data-test="todo"]').classes()).toContain('completed')
})

완료된 todo에 completed라는 클래스를 적용해줄 예정

<template>
  <div>
    <div
      v-for="todo in todos"
      :key="todo.id"
      data-test="todo"
      :class="[todo.completed ? 'completed' : '']"
    >
      {{ todo.text }}
      <input
        type="checkbox"
        v-model="todo.completed"
        data-test="todo-checkbox"
      />
    </div>

    <form data-test="form" @submit.prevent="createTodo">
      <input data-test="new-todo" v-model="newTodo" />
    </form>
  </div>
</template>

완성!

Arrange, Act, Assert

위 예제를 보면 각 테스트의 코드 사이에 개행이 존재하는 경우가 있음:

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

test('creates a todo', async () => {
  const wrapper = mount(TodoApp)

  await wrapper.get('[data-test="new-todo"]').setValue('New todo')
  await wrapper.get('[data-test="form"]').trigger('submit')

  expect(wrapper.findAll('[data-test="todo"]')).toHaveLength(2)
})

이 테스트는 개행으로 구별된 세 개의 단계 존재: arrange (준비), act (행동), assert (단언)

준비 단계 - 테스트를 위한 시나리오 셋업, 더 복잡한 테스트는 스토어 생성, DB 데이터 채워넣기 등 작업 필요
행동 단계 - 유저가 컴포넌트 또는 앱과 어떻게 상호작용할 지에 대한 시나리오의 시뮬레이션
단언 단계 - 우리가 기대하기에 현재 컴포넌트가 어떤 state를 가져야 하는지에 대한 단언

거의 모든 테스트는 이 세 단계를 따름, 이 가이드에서처럼 꼭 개행으로 나눠야 하는 것은 아니지만 이 세 단계를 염두에 두고 테스트를 작성하면 좋음

결론

mount()로 컴포넌트 렌더

get()findAll()로 DOM 쿼리

trigger()setValue로 유저 input 시뮬레이션

DOM 업데이트는 비동기로 이루어지므로 asyncawait 사용

테스트는 대개 준비, 행동, 단언의 세 단계로 구성됨

0개의 댓글