跳至主要内容

快速入門教學

讓我們直接開始吧!這份教學會帶你邊寫測試邊打造一個簡單的 Todo 應用,學習 Vue Test Utils(VTU)的基本用法。本教學會涵蓋:

  • 掛載元件
  • 查找元素
  • 填寫表單
  • 觸發事件

開始

我們先從一個只有一個 todo 的簡單 TodoApp 元件開始:

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

<script setup>
import { ref } from 'vue'

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

第一個測試:渲染一個 todo

我們要寫的第一個測試是驗證 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')
})

我們先引入 mount,這是 VTU 渲染元件的主要方式。用 test 函式宣告一個測試,並寫上簡短描述。testexpect 這兩個函式在大多數測試執行器(像 Jest)中都是全域可用的。如果你對 testexpect 不熟,可以參考 Jest 文件的簡單範例。

接著呼叫 mount 並傳入元件,這幾乎是每個測試都會做的事。慣例上我們會把結果存到 wrapper 變數,因為 mount 會回傳一個包裹元件的「wrapper」,提供許多方便的測試方法。

最後,我們用 expect 來斷言實際輸出是否符合預期。這裡我們用 get 找到 selector 為 data-test="todo" 的元素(在 DOM 裡會長這樣 <div data-test="todo">...</div>),然後用 text 取得內容,預期應該是 'Learn Vue.js 3'。

使用 data-test selector 不是強制的,但可以讓測試更穩定。class 和 id 很容易因為需求變動而更改,但 data-test 很明確告訴其他開發者這是給測試用的元素,不要隨便動。

讓測試通過

如果現在執行這個測試,會失敗並出現錯誤訊息:Unable to get [data-test="todo"]。因為我們還沒渲染任何 todo,所以 get() 找不到元素。讓我們把 <template> 改成會渲染 todos 陣列:

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

這樣一改,測試就會通過。恭喜你,完成了第一個元件測試!

新增 todo

接下來要讓使用者可以新增 todo。需要一個表單和輸入框,讓使用者輸入內容並送出。當表單送出時,應該會看到新的 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 渲染元件,並斷言一開始只有 1 個 todo,這樣最後測試才知道有多新增一個。

setValue 更新 <input> 的值。

更新完 <input> 後,用 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 setup>
import { ref } from 'vue'

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

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

這裡用 v-model 綁定 <input>,用 @submit 監聽表單送出,呼叫 createTodo 把新 todo 加進 todos 陣列。

雖然看起來沒問題,但執行測試會出錯:

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,像 triggersetValue。改成這樣:

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 了,接下來要讓使用者可以用 checkbox 勾選 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')
})

這個測試和前面很像,找到元素後用 setValue 操作 <input>

最後斷言 todo 元素有 completed 這個 class,這樣就能用 CSS 樣式標示完成狀態。

要讓這個測試通過,只要把 <template> 改成有 <input type="checkbox">,並在 todo 元素加上 class 綁定:

<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(斷言)。

  • Arrange:設定測試情境。複雜一點的例子可能要建立 Vuex store 或填資料庫。
  • Act:模擬使用者互動。
  • Assert:檢查元件狀態是否如預期。

幾乎所有測試都會遵循這三個階段。你不一定要用空行分隔,但寫測試時記得這三個步驟會很有幫助。


結論

  • mount() 渲染元件。
  • get()findAll() 查詢 DOM。
  • trigger()setValue() 可以模擬使用者輸入。
  • 更新 DOM 是非同步操作,記得用 async/await。
  • 測試通常分為三個階段:arrange、act、assert。