在上手開發一陣子後,總算是迎來撰寫單元測試的機會。由於測試工具的更迭速度比預期的快很多,這篇撰寫的當下是用 Vue3 + TS + Jest 來撰寫單元測試。
內容
單元測試的核心目的是要用「程式」來測試「程式」。換言之,你會需要多個轉換套件互相配合,讓測試工具 Jest 可以讀懂你的程式碼。不同的副檔名,會有可能需要不同的轉換套件。
設定檔
package.json
指令增加 test。測試相關的套件都安裝在 devDependencies
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
{ "script":{ "test": "jest" }, "devDependencies":{ "@babel/core": "^7.22.5", "@babel/plugin-transform-runtime": "^7.22.5", "@babel/preset-env": "^7.22.5", "@babel/preset-typescript": "^7.22.5", "@types/jest": "^29.5.2", "@vue/test-utils": "^2.4.0", "@vue/vue3-jest": "28", "babel-jest": "^29.5.0", "jest": "^29.5.0", "jest-css-modules-transform": "^4.4.2", "jest-environment-jsdom": "^29.5.0", "jest-html-reporter": "^3.10.1", "jest-transform-stub": "^2.0.0", "ts-jest": "^29.1.0", } } |
佈署指令
從 npm v7 發佈後,會預設使用 peerDependencies 安裝。雖說可以避免同一個名稱的套件被多次安裝,但不同的測試套件中,有可能會依賴同名套件但不同版本才能正常運作的情況。如此一來,就會回報套件衝突。
這回在 gitlab-ci 中遇上了此狀況,最簡單的解法就是將 npm install 後方加上 ”
.gitignore
新增 test-report.html 來避免測試生成的報告網頁上傳到遠端
babel.config.json
1 2 3 4 5 6 7 |
// 'presets' : 解決 json 文件、解決 esmodule 無法引入問題 // 'plugins' : 轉換 ES7 的 async/await 語法 { "presets": [["@babel/preset-env"], "@babel/preset-typescript"], "plugins": ["@babel/plugin-transform-runtime"] } |
.eslintrc
要加上 jest: true
1 2 3 4 5 |
module.exports = { env: { jest: true, } } |
.tsconfig
要告知 TypeScript 可以自動抓取 jest 的型別,不用每個檔案都個別引入
1 2 3 |
{ "types":["@types/jest"] } |
jest.config.js
moduleNameMapper: 當 jest 讀到符合的路徑規則時,轉譯成另一路徑或是使用特定的解析模組
moduleFileExtensions: 讓 jest 讀取的檔案
preset: jest 預設是用 babel 來編譯 ts, tsx,不過不會進行型別檢查。因此要改用 ts-jest
testEnvironment: 使用 jsdom,模擬瀏覽器的環境
testEnvironmentOptions: 的 customExportConditions:預設使用 node 和 node-addons 模組來執行
transformIgnorePatterns: 讀取到符合的規則字串時,忽略解析。這邊的模組是不支援 * 的
transform:讀取不同的附檔名時,用不同的解析器
testMatch:符合資料夾路徑內的 test 檔案才會執行
testResultsProcessor: 出報告的套件位置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
module.exports = { moduleNameMapper: { '^@/(.*)$': '<rootDir>/src/$1', '^.+.(css|styl|less|sass|scss|png|jpg|ttf|woff|woff2)$': 'jest-transform-stub', }, moduleFileExtensions: ['js', 'ts', 'vue'], preset: 'ts-jest', testEnvironment: 'jsdom', testEnvironmentOptions: { customExportConditions: ['node', 'node-addons'], }, transformIgnorePatterns: ['<rootDir>/node_modules/(?!lodash-es)'], transform: { '^.+\\.[j|t]sx?$': 'babel-jest', '^.+\\.vue?$': '@vue/vue3-jest', '^.+\\.tsx$': 'ts-jest', '^.+\\.(css|styl|less|sass|scss)$': 'jest-css-modules-transform', }, testMatch: [ '**/__test__/**/*.test.(js|jsx|ts|tsx)|**/__test__/*.(js|jsx|ts|tsx)', ], testResultsProcessor: './node_modules/jest-html-reporter', } |
心法
1. mount 你的 Vue 檔案時,可以用 config.global.mocks 來模擬某些公用方法,像是 $t (vue-i18n) 的套件方法
2. 若要完成單元測試,那盡量都是由外部傳入參數的 props 會是最簡單的,再搭配 wrapper.vm 來存取內部 ref, reactive 的變數。雖說這方法在 composition api 並不推薦,不過目前也沒想到其他方法
3. 當你發覺點擊的行為沒反應時,可以先用 wrapper.html() 的方法來查看當下渲染的內容正不正確
4. 測試工具多半都是去看當下渲染的 DOM 內容
注意事項
1. 測試區塊的劃分除了 JS 的 scope 以外,就是以 describe 為主。
2. 每一個 describe 中,請僅含一個 mount 或是 shallowMount 元件,否則會產生衝突。後者會把前者洗掉,造成 nextTick 時出錯
3. jest.useFakeTimers() 搭配 advanceTimersByTime ,可以測試 debounce()
4. jest.clearAllTimers() 建議放在 afterEach() 中進行
FAQ
1. Vue Test Utils 到底要用什麼版本?
與 Vue3 搭配要用 Vue Test Utils 2
2. 官方的 Vue jest 要用什麼版本?
可參閱 https://github.com/vuejs/vue-jest 說明:Vue3 且 Jest 安裝 28 以上版本的,要用 @vue/vue3-jest@28
3. nextTick 要什麼時候用?
當你的操作步驟中,遇上「因應某些資料改變,渲染的內容有更新」的情況時,那就要給一個 nextTick 了
參考資料
1. 【前端开发技巧】npm install xxxx –legacy-peer-deps到底做了些什么?
2. Jest + TypeScript:建置測試環境
3. Vue Test Utils
4. Vite+Vue3+TypeScript 使用 jest(后期添加)
5. jest、vue test utils 入门 vue3 + ts 组件测试
6. Vue-test-utils | Jest: How to handle dependencies?
7. ReferenceError: Vue is not defined while using shallowMount