Vue Unit Test|Vue單元測試 vue-cli

Wendy Chang
Wendy Loops
Published in
11 min readJul 11, 2024

Vue要來做單元測試了!需要安裝以下兩個套件:

  • Vue Test Utils
    Vue推薦使用的套件,可以不用渲染 DOM元素就可以模擬渲染後的結果
  • Jest
    JavaScript函式庫,可以用裡面的語法來測試

安裝起來!(Vue-cli)

vue add unit-jest
// 輸入這行指令就會安裝jest & Vue Test Utils

安裝後須重新設定一下config

// jest.config.js
module.exports = {
preset: "@vue/cli-plugin-unit-jest",
testMatch: ["**/src/**/*.spec.[jt]s?(x)"],
transform: {
"^.+\\.js$": "babel-jest",
"^.+\\.vue$": "@vue/vue3-jest",
}
};

testMatch: 這定義了 Jest 應該在哪裡尋找測試文件。它會匹配 src 目錄及其子目錄中所有以 .spec.js, .spec.ts, .spec.jsx, 或 .spec.tsx 結尾的文件。

transform: 這部分定義了如何轉換不同類型的文件:

  • "^.+\\.js$": "babel-jest": 使用 babel-jest 轉換所有 .js 文件。
  • "^.+\\.vue$": "@vue/vue3-jest": 使用 @vue/vue3-jest 轉換所有 .vue 文件。

package.json加入腳本

// package.json
{
{
scripts: {
"test:unit": "vue-cli-service test:unit --collectCoverage",
"test:watch": "vue-cli-service test:unit --watchAll"
}
}

--collectCoverage可以計算出測試覆蓋率,可以不用加
--watchAll每當存一次spec.js檔,就會重新執行npm

在terminal輸入 npm run test:unit 或是 npm run test:watch 就可以開始測試了。

在哪裡導入測試?

我都安裝好並且也了解3A之後,就開始想那我要從哪裡開始測試咧!因為有Vue Test Utils我就一直以為這跟e2e測試很類似,結果我被stub搞到發瘋後就決定重新來過,先專注於共用function的測試。

專案中剛好有一些共用function,例如我有一個製作UUID的function:

// UUID.js
export default function() {
var d = Date.now();
if (typeof performance !== 'undefined' && typeof performance.now === 'function'){
d += performance.now(); //use high-precision timer if available
}
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
var r = (d + Math.random() * 16) % 16 | 0;
d = Math.floor(d / 16);
return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
});
}

直接丟到AI產生AAA格式的測試檔 UUID.spec.js

import UUID from "@/lib/UUID.js";

describe('UUID Generator', () => {

it('should generate a valid UUID', () => {
const uuid = UUID();
expect(uuid).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i);
});

it('should generate unique UUIDs', () => {
const uuids = new Set();
for (let i = 0; i < 1000; i++) {
uuids.add(UUID());
}
expect(uuids.size).toBe(1000);
});

it('should have the correct version (4)', () => {
const uuid = UUID();
const versionChar = uuid.split('-')[2][0];
expect(versionChar).toBe('4');
});

it('should have the correct variant', () => {
const uuid = UUID();
const variantChar = parseInt(uuid.split('-')[3][0], 16);
expect(variantChar >= 8 && variantChar <= 11).toBeTruthy();
});

it('should be consistent in format', () => {
for (let i = 0; i < 100; i++) {
const uuid = UUID();
const parts = uuid.split('-');
expect(parts.length).toBe(5);
expect(parts[0].length).toBe(8);
expect(parts[1].length).toBe(4);
expect(parts[2].length).toBe(4);
expect(parts[3].length).toBe(4);
expect(parts[4].length).toBe(12);
}
});

// mock Performance 模擬產生的performance & 時間
it('should use high-precision timer if available', () => {
const originalPerformance = global.performance;
const originalDateNow = Date.now;

const mockPerformanceNow = jest.fn(() => 123.456);
const mockDateNow = jest.fn(() => 1000000);

// 使用 Object.defineProperty 來模擬 performance
Object.defineProperty(global, 'performance', {
value: { now: mockPerformanceNow },
writable: true,
configurable: true
});
Date.now = mockDateNow;

UUID();

expect(mockDateNow).toHaveBeenCalled();
expect(mockPerformanceNow).toHaveBeenCalled();


// 恢復原始object
global.performance = originalPerformance;
Date.now = originalDateNow;
});


// 如果performance是undefined,就不加上d += performance.now()了
it('should work without performance.now()', () => {
const originalPerformance = global.performance;
global.performance = undefined;

const uuid = UUID();
expect(uuid).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i);

global.performance = originalPerformance;
});
});

跑一次看看,排除一些變數設定錯誤的問題,一個共用function的測試就完成了。

接著把其他的共用function都拉出來寫unit test,就可以完成最初步的單元測試了。

Vuex store單元測試

完成了共用function的單元測試,接下來就可以測共用的vuex store了,單元測試的重點就是「測試共用的function」。

確認原始的store中是否export出store跟createStore

// store.js
import { createStore } from "vuex";

export const store = {
state(){
return {
user: '',
}
},
// ...
}

// 專案要用的vuex store
export default createStore(store);

接著回到store.spec.js中,先在beforeEach製作一個新的$store(加上$是為了與原始的store區隔)

  • beforeEach:每次測試前都要執行
import { createStore } from 'vuex';
import { store } from "./store"

describe('it store commit', () => {
let $store; // 放在外層之後測試才能取用

beforeEach(() => {
// 在每個測試之前創建一個新的 store 實例
$store = createStore(store);
});
})

接著寫測試(其實也可以丟給AI寫XD)

    describe('initial state', () => {
it('should have the correct initial state', () => {
expect($store.state).toEqual({
user: ''
});
});
});

把state、mutations、getters、actions都補齊,確認$store沒有問題,單元測試就完成了。

丟到gitlab CI

在根目錄新增一個檔案.gitlab-ci.yml(直接在gitlab新增檔案也可以!注意branch)

stages:
- test

unit-test:
image: cypress/browsers:node-20.9.0-chrome-118.0.5993.88-1-ff-118.0.2-edge-118.0.2088.46-1
stage: test
tags:
# 因為公司有買我就用這個了
- saas-linux-2xlarge-amd64
script:
# install dependencies
- npm ci
# start the server in the background
- npm run test:unit

npm ci跟npm i不一樣,npm i會針對package.json來還原套件,npm ci針對 package-lock.json 來還原套件,測試時用ci安裝會比較快,也不會有版本不一致的問題。

參考資料:npm ci 與 npm install 差異

之後只要branch上有新的commit更新,gitlab就會自動執行單元測試了!

我等那個passed等得好苦

我在單元測試中鬼打牆超級久,因為一直不知道從哪裡開始測,還把單元測試測成e2e test😭😭😭我就以為每一頁都要測單元測試啊!!(實際上測每一頁的叫做e2e test,而且也不用每一頁都測)

一直不知道測試的意義在哪,直到我寫e2e,測試一直報錯才發現有個API一直打不到,所以頁面一直渲染不出來😭😭😭這時候才懂測試的好,但我還沒品嘗到寫單元測試的美好啦🥵🥵🥵至少通過後Passed很爽!?

今天晚上請朋友幫我代購的前端測試指南:策略與實踐終於即將到手,晚上來翻看看,再跟現在寫的這篇文章比對,看看自己有沒有誤會些什麼,自學測試還是有點辛苦的😭😭😭

--

--

Wendy Chang
Wendy Loops

什麼都寫ㄉ前端工程師 / 影片剪輯師 / 自媒體經營者