Vue.Js單元測試綜合指南

Vue.Js單元測試綜合指南

網路開發中的測試對於確保網路應用程式的可靠性、功能性和質量至關重要。通過系統地檢查功能、效能和使用者體驗等各個方面,測試有助於識別和糾正錯誤、缺陷或意外行為。它能提高使用者對應用程式的整體滿意度和信任度,並通過在開發過程中儘早發現問題來節省時間和金錢。本文為更深入地探討單元測試在開發穩健可靠的基於網路的解決方案中發揮的關鍵作用奠定了基礎。

Vue.js 中的單元測試需要使用 VitestVue Test UtilsJest 等關鍵工具,以確保測試的徹底性和可靠性。Vitest 是 Vue.js 的官方實用程式庫,它可以模擬 Vue 元件,並對元件進行隔離測試。首先,在終端中鍵入 Vue CLI 命令,建立一個新的 Vue 專案。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
npm create vue@latest
npm create vue@latest
npm create vue@latest

然後,系統會提示您選擇應用程式所需的所有依賴項。

應用程式所需的所有依賴項

接下來,選擇 Vue RouterPinia、Vitest,然後按回車鍵。你也可以克隆這個 boilerplate。這裡已經新增了所需的一切。

編寫第一個 Vue.js 單元測試

在本示例中,我們將在 src/components/ 中建立一個名為 BaseButton.vue 的按鈕元件。該按鈕接受文字 prop,點選後會向使用者顯示新文字。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<template>
<button @click="handleButton">
{{ text }}
</button>
<p>{{ textToRender }}</p>
</template>
<script>
import { ref } from 'vue'
export default {
props: {
text: String
},
setup() {
const textToRender = ref('default')
const handleButton = () => {
textToRender.value = 'Hi there'
}
return {
handleButton,
textToRender
}
}
}
</script>
<template> <button @click="handleButton"> {{ text }} </button> <p>{{ textToRender }}</p> </template> <script> import { ref } from 'vue' export default { props: { text: String }, setup() { const textToRender = ref('default') const handleButton = () => { textToRender.value = 'Hi there' } return { handleButton, textToRender } } } </script>
<template>
<button @click="handleButton">
{{ text }}
</button>
<p>{{ textToRender }}</p>
</template>
<script>
import { ref } from 'vue'
export default {
props: {
text: String
},
setup() {
const textToRender = ref('default')
const handleButton = () => {
textToRender.value = 'Hi there'
}
return {
handleButton,
textToRender
}
}
}
</script>

點選後會向使用者顯示新文字

我們將為該元件的 propsstate 和 user 互動建立一個基本的穿行測試。接下來,在你的測試資料夾中建立一個副檔名為”.test.js” 的檔案,然後按照下面的示例實現你的第一個測試。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import BaseButton from 'from-your-component-location'
describe('MyButton.vue', () => {
it('renders button text from props', () => {
const text = 'Click Me'
const wrapper = mount(BaseButton, {
props: { text }
})
expect(wrapper.text()).toMatch(text)
})
})
import { describe, it, expect } from 'vitest' import { mount } from '@vue/test-utils' import BaseButton from 'from-your-component-location' describe('MyButton.vue', () => { it('renders button text from props', () => { const text = 'Click Me' const wrapper = mount(BaseButton, { props: { text } }) expect(wrapper.text()).toMatch(text) }) })
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import BaseButton from 'from-your-component-location'
describe('MyButton.vue', () => {
it('renders button text from props', () => {
const text = 'Click Me'
const wrapper = mount(BaseButton, {
props: { text }
})
expect(wrapper.text()).toMatch(text)
})
})

在本示例中,為 BaseButton.vue 元件建立了一個測試套件。該套件中的一個測試用例將驗證按鈕文字是否使用所提供的道具正確呈現。@vue/test-utils 中的 mount 函式用於掛載元件進行測試。測試使用 Vitest 的語法執行,其中包括測試套件建立函式(如 describeit )以及斷言函式(如 expect ),以便在測試過程中驗證特定的元件行為。接下來,我們將編寫一個測試來模擬使用者點選按鈕時對使用者介面的更新。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
it('updates text when button is clicked', async () => {
const wrapper = mount(BaseButton, {
props: { text: 'Initial' }
})
await wrapper.find('button').trigger('click')
expect(wrapper.find('p').text()).toBe('Hi there')
})
it('updates text when button is clicked', async () => { const wrapper = mount(BaseButton, { props: { text: 'Initial' } }) await wrapper.find('button').trigger('click') expect(wrapper.find('p').text()).toBe('Hi there') })
it('updates text when button is clicked', async () => {
const wrapper = mount(BaseButton, {
props: { text: 'Initial' }
})
await wrapper.find('button').trigger('click')
expect(wrapper.find('p').text()).toBe('Hi there')
})

在該測試中使用了一個非同步函式,通過觸發對掛載元件中按鈕元素的點選來模擬按鈕點選事件。在這種互動之後,測試希望元件 p 標籤中的內容是 “Hi there”,從而驗證按鈕點選動作後的預期文字更新。這個非同步測試確保了元件能正確響應使用者互動,在點選按鈕時更改顯示的文字。接下來,我們還將編寫一個測試來模擬狀態。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
it('verifies default state', () => {
const wrapper = mount(BaseButton)
expect(wrapper.find('p').text()).toBe('default')
})
it('verifies default state', () => { const wrapper = mount(BaseButton) expect(wrapper.find('p').text()).toBe('default') })
it('verifies default state', () => {
const wrapper = mount(BaseButton)
expect(wrapper.find('p').text()).toBe('default')
})

此示例通過在沒有任何初始道具的情況下安裝按鈕元件,並確保 p 標籤中的文字顯示 “default”,來驗證按鈕元件的預設狀態。這將確保元件在任何道具初始化之前都能按照預期的方式執行。現在您已完成測試套件,請在終端上執行此命令。如果一切順利,測試就會通過。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
npm run test:unit
npm run test:unit
npm run test:unit

顯示測試已成功通過

上圖顯示測試已成功通過。元件行為測試取決於你是否知道如何使用斷言和期望來驗證和確保獲得預期結果。它們涉及建立有關元件功能預期結果的具體宣告。以下是您應該瞭解的斷言和期望:

  • 斷言用於驗證條件或預期結果。
  • 期望則為假設為真設定了準則。斷言的例子有 toEqualtoContaintoBeDefined等。

模擬依賴關係和應用程式介面

模擬是單元測試中的一項重要技術,因為它允許開發人員模擬元件所依賴的外部依賴關係、函式或服務。當依賴關係複雜、涉及網路請求或不直接受測試環境控制時,模擬就顯得至關重要。模擬建立這些依賴關係的受控例項,允許開發人員在測試過程中預測它們的行為、響應和結果,而無需執行真實邏輯或依賴真實的外部系統。假設你有一個從外部 API 獲取資料的函式 getDataFromAPI

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
export const getDataFromAPI = () => {
//Returns the response data;
}
export const getDataFromAPI = () => { //Returns the response data; }
export const getDataFromAPI = () => {
//Returns the response data;
}

在主元件中,您可以使用 getDataFromAPI 這個函式

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
import { getDataFromAPI } from './eventService'
export async function processDataFromAPI() {
try {
const data = await getDataFromAPI()
return data
} catch (e) {
//handle error;
}
}
import { getDataFromAPI } from './eventService' export async function processDataFromAPI() { try { const data = await getDataFromAPI() return data } catch (e) { //handle error; } }
import { getDataFromAPI } from './eventService'
export async function processDataFromAPI() {
try {
const data = await getDataFromAPI()
return data
} catch (e) {
//handle error;
}
}

要測試 processDataFromAPI,可以模擬 getDataFromAPI 函式。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
import { expect, vi, test } from 'vitest'
import { processDataFromAPI } from './mainComponent'
import * as eventService from '../eventService'
vi.mock('./eventService.js')
test('processDataFromAPI function test', async () => {
const mockData = 'Mocked Data'
eventService.getDataFromAPI.mockResolvedValue(mockData)
const result = await processDataFromAPI()
expect(eventService.getDataFromAPI).toHaveBeenCalled()
expect(result).toBe('Mocked Data')
})
import { expect, vi, test } from 'vitest' import { processDataFromAPI } from './mainComponent' import * as eventService from '../eventService' vi.mock('./eventService.js') test('processDataFromAPI function test', async () => { const mockData = 'Mocked Data' eventService.getDataFromAPI.mockResolvedValue(mockData) const result = await processDataFromAPI() expect(eventService.getDataFromAPI).toHaveBeenCalled() expect(result).toBe('Mocked Data') })
import { expect, vi, test } from 'vitest'
import { processDataFromAPI } from './mainComponent'
import * as eventService from '../eventService'
vi.mock('./eventService.js')
test('processDataFromAPI function test', async () => {
const mockData = 'Mocked Data'
eventService.getDataFromAPI.mockResolvedValue(mockData)
const result = await processDataFromAPI()
expect(eventService.getDataFromAPI).toHaveBeenCalled()
expect(result).toBe('Mocked Data')
})

在本示例中,程式碼演示瞭如何使用 mock 來驗證服務函式 processDataFromAPI 的功能。 vi.mock 函式用 eventService 的模擬實現替換了實際的 getDataFromAPI 呼叫。當指定 eventService.getDataFromAPI.mockResolvedValue(mockData) 時,被模擬的函式將使用設定值 “Mocked Data” 進行解析。以下是測試結果:

驗證對模擬資料的處理

下面的測試將檢查是否呼叫了模擬的 getDataFromAPI,並驗證對模擬資料的處理,從而確保 processDataFromAPI 的正確執行。該測試可有效隔離並檢查 processDataFromAPI 的行為,而無需依賴實際的 API 呼叫來獲取資料。

高階測試技術

在本節中,我們將討論如何在應用程式的某個關鍵部分利用測試。您將瞭解到應用於 Pinia Store 和 Vue 路由的各種測試方法。測試 Pinia 商店的主要目的是評估狀態和操作的行為和功能。調查的關鍵領域包括確保狀態變化準確反映商店中的操作。因此,測試要確認操作能準確執行對商店狀態的突變和邏輯操作。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
import { defineStore } from 'pinia'
export const useCounterStore = defineStore({
id: 'counter',
state: () => ({
count: 0
}),
actions: {
increment() {
this.count++
},
decrement() {
this.count--
}
}
})
import { defineStore } from 'pinia' export const useCounterStore = defineStore({ id: 'counter', state: () => ({ count: 0 }), actions: { increment() { this.count++ }, decrement() { this.count-- } } })
import { defineStore } from 'pinia'
export const useCounterStore = defineStore({
id: 'counter',
state: () => ({
count: 0
}),
actions: {
increment() {
this.count++
},
decrement() {
this.count--
}
}
})

在這個示例中,儲存為計數器定義了一個簡單的狀態,並通過相應的操作來遞增和遞減計數。現在讓我們建立一個 CounterStore.spec.js

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
import { setActivePinia, createPinia } from 'pinia'
import { expect, describe, it, beforeEach } from 'vitest'
import { useCounterStore } from '../../stores/counter'
describe('Counter Store', () => {
beforeEach(() => {
setActivePinia(createPinia())
})
it('increments the count', () => {
const counterStore = useCounterStore()
counterStore.increment()
expect(counterStore.count).toBe(1)
})
it('decrements the count', () => {
const counterStore = useCounterStore()
counterStore.decrement()
expect(counterStore.count).toBe(-1)
})
it('increments the count twice', () => {
const counterStore = useCounterStore()
counterStore.increment()
counterStore.increment()
expect(counterStore.count).toBe(2)
})
})
import { setActivePinia, createPinia } from 'pinia' import { expect, describe, it, beforeEach } from 'vitest' import { useCounterStore } from '../../stores/counter' describe('Counter Store', () => { beforeEach(() => { setActivePinia(createPinia()) }) it('increments the count', () => { const counterStore = useCounterStore() counterStore.increment() expect(counterStore.count).toBe(1) }) it('decrements the count', () => { const counterStore = useCounterStore() counterStore.decrement() expect(counterStore.count).toBe(-1) }) it('increments the count twice', () => { const counterStore = useCounterStore() counterStore.increment() counterStore.increment() expect(counterStore.count).toBe(2) }) })
import { setActivePinia, createPinia } from 'pinia'
import { expect, describe, it, beforeEach } from 'vitest'
import { useCounterStore } from '../../stores/counter'
describe('Counter Store', () => {
beforeEach(() => {
setActivePinia(createPinia())
})
it('increments the count', () => {
const counterStore = useCounterStore()
counterStore.increment()
expect(counterStore.count).toBe(1)
})
it('decrements the count', () => {
const counterStore = useCounterStore()
counterStore.decrement()
expect(counterStore.count).toBe(-1)
})
it('increments the count twice', () => {
const counterStore = useCounterStore()
counterStore.increment()
counterStore.increment()
expect(counterStore.count).toBe(2)
})
})

這段程式碼設定了用於測試的 Vue 路由,並定義了一個簡單的路由保護邏輯。路由只有一個路由 /about,其 requiresAuth 元欄位設定為 true。路由保護函式 beforeEach 會檢查路由是否需要授權。出於演示目的,它模擬了一次身份驗證檢查; isAuthenticated 設定為 false,將未通過身份驗證的使用者重定向回主路由 '/' ,同時允許訪問不需要授權的路由。測試套件包括兩個測試,一個是確保授權後訪問受保護路由,另一個是驗證未經授權的使用者是否被拒絕訪問受保護路由,模擬路由保護的行為。根據應用程式的身份驗證邏輯調整 isAuthenticated 條件。這是一張顯示測試結果的圖片。執行測試,應該會得到相同的結果。

顯示測試結果的圖片

接下來,我們將學習如何測試 Vue 路由。在測試 Vue 路由導航和路由保護時,確保導航行為和路由保護機制按預期執行至關重要。使用 Vitest 這樣的測試庫,您可以模擬路由操作並驗證結果。下面是測試導航和路由保護的示例程式碼。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
import { expect, it, describe } from 'vitest'
import { mount } from '@vue/test-utils'
import { createRouter, createWebHistory } from 'vue-router'
import AboutView from '../../views/AboutView.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/about',
name: 'about',
component: AboutView,
meta: { requiresAuth: true }
}
]
})
router.beforeEach((to, from, next) => {
if (to.meta.requiresAuth) {
const isAuthenticated = false
if (isAuthenticated) {
next()
} else {
next('/')
}
} else {
next()
}
})
describe('Vue Router Navigation', () => {
it('navigates to a protected route with proper authorization', async () => {
await router.push('/about')
await router.isReady()
const wrapper = mount(AboutView, {
global: {
plugins: [router]
}
})
expect(wrapper.findComponent(AboutView).exists()).toBe(true)
})
it('prevents access to a protected route without authorization', async () => {
await router.push('/about')
expect(router.currentRoute.value.fullPath).not.toBe('/about')
})
})
import { expect, it, describe } from 'vitest' import { mount } from '@vue/test-utils' import { createRouter, createWebHistory } from 'vue-router' import AboutView from '../../views/AboutView.vue' const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes: [ { path: '/about', name: 'about', component: AboutView, meta: { requiresAuth: true } } ] }) router.beforeEach((to, from, next) => { if (to.meta.requiresAuth) { const isAuthenticated = false if (isAuthenticated) { next() } else { next('/') } } else { next() } }) describe('Vue Router Navigation', () => { it('navigates to a protected route with proper authorization', async () => { await router.push('/about') await router.isReady() const wrapper = mount(AboutView, { global: { plugins: [router] } }) expect(wrapper.findComponent(AboutView).exists()).toBe(true) }) it('prevents access to a protected route without authorization', async () => { await router.push('/about') expect(router.currentRoute.value.fullPath).not.toBe('/about') }) })
import { expect, it, describe } from 'vitest'
import { mount } from '@vue/test-utils'
import { createRouter, createWebHistory } from 'vue-router'
import AboutView from '../../views/AboutView.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/about',
name: 'about',
component: AboutView,
meta: { requiresAuth: true }
}
]
})
router.beforeEach((to, from, next) => {
if (to.meta.requiresAuth) {
const isAuthenticated = false
if (isAuthenticated) {
next()
} else {
next('/')
}
} else {
next()
}
})
describe('Vue Router Navigation', () => {
it('navigates to a protected route with proper authorization', async () => {
await router.push('/about')
await router.isReady()
const wrapper = mount(AboutView, {
global: {
plugins: [router]
}
})
expect(wrapper.findComponent(AboutView).exists()).toBe(true)
})
it('prevents access to a protected route without authorization', async () => {
await router.push('/about')
expect(router.currentRoute.value.fullPath).not.toBe('/about')
})
})

這段程式碼設定了用於測試的 Vue 路由,並定義了一個簡單的路由保護邏輯。路由只有一個路由 /about,其 requiresAuth 元欄位設定為 true。路由保護函式 beforeEach 會檢查路由是否需要授權。出於演示目的,它模擬了一次身份驗證檢查; isAuthenticated 設定為 false ,將未通過身份驗證的使用者重定向回主路由 '/' ,同時允許訪問不需要授權的路由。測試套件包括兩個測試,一個是確保授權後訪問受保護路由,另一個是驗證未經授權的使用者是否被拒絕訪問受保護路由,模擬路由保護的行為。根據應用程式的身份驗證邏輯調整 isAuthenticated 條件。這是一張顯示測試結果的圖片。執行測試,應該會得到相同的結果。

一張顯示測試結果的圖片

測試覆蓋率

測試覆蓋率是軟體開發中的一個重要指標,因為它衡量了測試套件對程式碼庫的徹底檢查程度。它表明測試使用了多少程式碼。提高測試覆蓋率可確保程式碼的更多部分得到驗證,從而降低未檢測到錯誤的可能性。它還有助於識別程式碼中缺乏測試覆蓋率的區域,使開發人員能夠將測試工作集中在這些關鍵部分。Vitest 自帶原生覆蓋率支援,只需幾步就能獲得程式碼庫的覆蓋率報告。在終端上執行此命令,為測試新增覆蓋率。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
npm i -D @vitest/coverage-v8
npm i -D @vitest/coverage-v8
npm i -D @vitest/coverage-v8

完成安裝後,將其複製到您的終端,您將得到一份覆蓋範圍報告。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
npm run coverage
npm run coverage
npm run coverage

覆蓋率報告

該覆蓋率報告概述了程式碼庫中的測試覆蓋範圍,提供了關鍵指標:%Stmts、%Branch、%Funcs 和 %Lines。這些百分比表示測試覆蓋的程式碼、決策分支、函式和行數。 HelloWorld.vueAboutView.vue 等檔案具有完整的覆蓋率,而 eventService.jsmainComponent.js 則顯示了部分覆蓋率,尤其是在特定行中,突出了需要改進的地方。本報告可作為優先測試工作的指南,引導人們關注未覆蓋的部分,以進行更全面、更可靠的測試。

小結

總之,前端應用程式測試對於確保程式碼的可靠性、穩定性和正確性至關重要。它能及早發現錯誤,保持程式碼的高質量,並促進可擴充套件性。團隊通過實施日常測試來確保軟體的穩健性和可維護性,從而獲得更好的使用者體驗和整體軟體產品。

評論留言