Vite 單元測試與設置

首先先建立Vite專案 Vite 安裝 typescript

安裝範例是 Vite7 Vue@3.5.17 TypeScript@5.8.3 範例

在專案根目錄安裝vitest @vue/test-utils jsdom

建立專案時要加入單元測試必須在Vitest 需要 Vite >=v5.0.0 和 Node >=v18.0.0

安裝測試工具
  • vitest:單元測試框架(提供了執行測試的環境、斷言、隔離庫⋯⋯等等功能與 API)
  • @vue/test-utils:測試 Vue 元件的工具
  • jsdom:讓我們可以在 Node 環境模擬出瀏覽器中的 DOM 環境(方便測試)
1
2
cd 到專案
npm install -D vitest @vue/test-utils jsdom

在專案根目錄安裝vitest @vue/test-utils jsdom

jsdom:是一個可以使用類似dom元件操作方式來操作html text的工具
在 ~專案根目錄/package.json
執行腳本指令新增單元測試所需要的指令
scripts加入
“test”: “vitest –environment jsdom”,(啟動單元測試指令時環境內的jsdom)

設定檔 package.json行腳本指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
"scripts": {
"dev": "vite",
"build": "vue-tsc -b && vite build",
"preview": "vite preview",
"test": "vitest --environment jsdom"
},
"dependencies": {
"vue": "^3.5.17"
},
"devDependencies": {
"@vitejs/plugin-vue": "^6.0.0",
"@vue/test-utils": "^2.4.6",
"@vue/tsconfig": "^0.7.0",
"jsdom": "^26.1.0",
"typescript": "~5.8.3",
"vite": "^7.0.4",
"vitest": "^3.2.4",
"vue-tsc": "^2.2.12"
}

範例:子元件 mount,props
在 src/components/內新增HelloWorld.spec.js

解釋名詞:mount
mountVue 中的 Test Utils 是測試 Vue 元件的關鍵函數。 它是創建 Vue 應用程式的主要方法,該應用程式在測試環境中託管和渲染被測元件。

describe & it 輔助 API
準備 => 操作 => 斷言

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//引入描述、它、斷言 如果在使用
import { describe, it, expect } from 'vitest'
//引入測試實用程式
import { mount } from '@vue/test-utils'
//引入元件
import HelloWorld from './HelloWorld.vue'

describe('HelloWorld', () =>
{
it('1+1 應該是2', () => {
expect(1 + 1).toBe(2)
})

it('正確渲染', () => {
const wrapper = mount(HelloWorld, { props: { msg: 'Hello Vitest' } })
expect(wrapper.text()).toContain('Hello Vitest')
})

it('在此測試檔案中使用 jsdom', () => {
const element = document.createElement('div')
element.innerHTML = '<p>Hello, HTML!</p>'
expect(element.innerHTML).toBe('<p>Hello, HTML!</p>')
})
})

執行啟動單元測試指令

1
npm run test

github安裝單元測試與執行啟動單元測試指令

得到以下

github

單元測試覆蓋

Vitest 支援兩種程式碼覆蓋率引擎,v8 和 Istanbul。
v8 使用Chrome v8 引擎中內建的程式碼覆蓋率測量。
v8 的一大優點是不需要預先檢測和轉譯。
v8 無法區分 if 語句、三元條件或 for 迴圈中的條件。它將它們全部突出顯示為條件表達式。

Istanbul是一個歷史悠久的 JavaScript 測試覆蓋率工具。
Istanbul檢測需要使用 Babel 插件實現轉譯過程。幸運的是,一切都由 Vitest 為您處理。
Istanbul的一大優勢是偵測發生在單行程式碼的層級。您應該得到非常精確的結果。只有您感興趣的程式碼才會被偵測。

安裝@vitest/coverage-v8 與vitest/coverage-istanbul指令

1
2
npm install -D @vitest/coverage-v8
npm install -D @vitest/coverage-istanbul

維測試介面

1
npm install -D @vitest/ui 

vitest 在typescript vite.config 的設置

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

/// <reference types="vitest/config" />
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue'
//型別設置
interface vitestConfigType {
include: string[],
globals: boolean,
environment: string,
exclude:string[],
coverage: object,
}
//定義vitestConfig
const vitestConfig: vitestConfigType = {
//默认值: ['**/*.{test,spec}-d.?(c|m)[jt]s?(x)']
// 在這裡加入測試設定:.spec.js類型都執行單元測試
include:['**/*.spec.js'],
globals: true,//全域:就不需要顯示引入 vitest 測試相關的 API,讓測試看起來更乾淨
environment: "jsdom",
//匹配排除测试文件,以下是預設
exclude:['**/node_modules/**', '**/dist/**', '**/cypress/**', '**/.{idea,git,cache,output,temp}/**'],
coverage: {
all: true,
//默認v8
provider: 'v8',
reporter: ['text', 'json', 'html'],
include: ['src/**.{vue,ts}'],
//排除
exclude: ['**/utils/users.ts']
}
};


export default defineConfig({
plugins: [
vue()
],
//設置vitestConfig
test:vitestConfig,
})

在package.json 修改以下

1
2
3
4
"scripts": {
"test": "vitest",
"coverage": "vitest --coverage"
},

執行啟動單元測試指令

1
2
npm run test
npm run coverage
執行啟動npm run coverage 後專案根目錄會產生了一個coverage 資料夾內index.html即是以下圖片 覆蓋率列表 所有文件 38.59% Statements(聲明) 22/570% Branches (分行) 0/20% Functions(函式) 0/338.59% Lines (行) 22/57 單元測試覆蓋率,維測試介面與 設置

使用data-* 作為選擇目標好處最主要在於顯著標記測試內容

在生產環境刪除 data-* attribute
若想在 vitest 中移除 data-* 也非常的簡單,我們只需要在 vite.config.js 設定中,針對 Vue 底下的編譯選項做一些調整即可(底下示範的版本為移除 data-test,若使用其他命名請自行調整囉):
vite.config.js

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
const isProd = process.env.NODE_ENV === 'production'
const removeDataTestAttrs = (node) => {
const NodeTypes = Object.freeze({
ELEMENT: 1,
ATTRIBUTE: 6,
})
if (node.type === NodeTypes['ELEMENT']) {
node.props = node.props.filter((prop) => (prop.type === NodeTypes['ATTRIBUTE'] ? prop.name !== 'data-test' : true)) // 請自行替換命名 data-test
}
}

export default defineConfig(() => {
return {
plugins: [
vue({
template: {
compilerOptions: {
nodeTransforms: isProd ? [removeDataTestAttrs] : [],
},
},
}),
]
}
})

模擬 HTTP 請求

組件有串接Api時

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
GuessAge.vue
<template>
<h1 data-test="title">{{ title }}</h1>
<div class="card">
<div style="width:400px;height:130px;margin-top:20px;border-style: dotted;" >
<br>
<span>Firsthand: {{firstname}}</span> <br>
<span>Age: {{age}}</span>

</div>
<label> Enter Firstname </label><br>
<input type="text" v-model="search" style="font-size:20px;border-radius:10px; border:2px solid red"/>
<a style="width:50px;height: 50px; background:red;"
data-test="getAge"
@click="getAge">Guess Age</a>
<input type="radio" value="pop"> <label>Save my data</label>
</div>
</template>

<script setup lang="ts">
import { ref,computed } from 'vue'

const props = defineProps({
title:{type:String},
})
const search = ref<string>('');
const age = ref<string>('');
const firstname = ref<string>('');
const getAge =() => {
fetch('https://api.agify.io/?name='+ search.value)
.then(response => response.json())
.then(data => {
age.value = data.age
firstname.value = data.name
search.value=""
})
}
</script>

安裝 msw

1
npm install msw --save-dev

建立假資料
新增src/mocks/handlers.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// src/mocks/handlers.js
//模擬 HTTP 請求
import { http, HttpResponse } from 'msw'
const api = "https://api.agify.io/";
export const restHandlers = [
http.get(api, (req, res, ctx) =>
{
const query = {
age: 55,
name: "tope"
}
console.log('ctx.json',ctx.json([ query]))
return res(ctx.status(200), ctx.json([
query
]))
}),
]

攔截 http 請求
生成地方

1
2
3
4
//
npx msw init 生成模擬Api資料夾 --save
//這時候就會在 public 資料夾下建立一個 mockServiceWorker.js 檔案,裏面就有攔截 http request 的程式碼了
npx msw init ./public --save

生成模擬Api資料夾
參考資料
GuessAge測試文件
GuessAge測試文件

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
//GuessAge.spec.js
import { describe, it, expect, afterEach, beforeAll, afterAll, beforeEach } from 'vitest'
import { setupServer } from 'msw/node'
import { mount } from "@vue/test-utils";
import GuessAge from "../GuessAge.vue";
//引入Api模擬
import { restHandlers } from "../../mocks/handlers";

const server = setupServer(...restHandlers)
// 在所有測試之前啟動伺服器
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }))
// 所有測試後關閉伺服器
afterAll(() => server.close())
// 每次測試後重置處理程序“對於測試隔離很重要”
afterEach(() => server.resetHandlers())
describe('GuessAge', () =>
{
let wrapper
const title = 'Guess User Age App'
if (typeof document !== 'undefined') {
beforeEach(() =>
{
wrapper = mount(GuessAge, {
props: {
title
}
})
})
}
it("測試 GuessAge 元件 props title", async () =>
{
if (typeof document !== 'undefined') {
expect(wrapper.find('[data-test="title"]').text()).toBe(title);

}
});
it("測試資料是否為函數", () =>
{
expect(typeof GuessAge.data).toBe("undefined");
});
it('快照 UI 測試', () =>
{
if (typeof document !== 'undefined') {
const wrapper = mount(GuessAge, {});
expect(wrapper.text()).toMatchSnapshot()
}
})
it("找到按鈕", () =>
{
if (typeof document !== 'undefined') {

expect(wrapper.find('[data-test="getAge"]').exists()).toBe(true);
}
});

it("按鈕點擊", async () =>
{
if (typeof document !== 'undefined') {
const ac = await wrapper.get('[data-test="getAge"]').trigger("click")
expect(wrapper.vm.search).toEqual("")
}
})
})

在元件測試中模擬 DOM API

安裝happy-dom

1
npm install happy-dom@6.0.4
1
npm i -D @vitest/coverage-istanbul

目錄

參考資料