什么是Vitest?
自從 尤大 的構(gòu)建工具Vite獲得了巨大的人氣,現(xiàn)在有了一個(gè)由它驅(qū)動(dòng)的極快的單元測(cè)試框架。Vitest。
Vitest 與 Jest 兼容,具有開箱即用的 ESM、Typescript 和 JSX 支持,并且由 esbuild 提供支持。它在測(cè)試過程中使用 Vite 開發(fā)服務(wù)器來轉(zhuǎn)換你的文件,并監(jiān)聽你的應(yīng)用程序的相同配置(通過vite.config.js),從而消除了使用Jest等測(cè)試替代品所涉及的重復(fù)工作。
為什么選擇Vitest?
Vite是一個(gè)構(gòu)建工具,旨在為現(xiàn)代 web 項(xiàng)目提供更快、更精簡的開發(fā)體驗(yàn),它開箱即用,支持常見的 web 模式、glob導(dǎo)入和 SSR 等功能。它的許多插件和集成正在促進(jìn)一個(gè)充滿活力的生態(tài)系統(tǒng)。
但這導(dǎo)致了一個(gè)新問題:如何在Vite上編寫單元測(cè)試。
將Jest等框架與Vite一起使用,導(dǎo)致Vite和Jest之間有很多重復(fù)的配置,而 Vitest 解決了這一問題,它消除了為我們的應(yīng)用程序編寫單元測(cè)試所需的額外配置。Vitest 使用與 Vite 相同的配置,并在開發(fā)、構(gòu)建和測(cè)試時(shí)共享一個(gè)共同的轉(zhuǎn)換管道。它還可以使用與 Vite 相同的插件API進(jìn)行擴(kuò)展,并與Jest的API兼容,以方便從Jest遷移,而不需要做很多重構(gòu)工作。
因此,Vitest 的速度也非???。
如何使用 Vitest 來測(cè)試組件
安裝 Vitest
在項(xiàng)目中使用 Vitest 需要 Vite >=v2.7.10 和 Node >=v14 才能工作。
可以使用 npm、yarn 或 pnpm 來安裝 Vitest,根據(jù)自己的喜好,在終端運(yùn)行以下命令:
NPM
npm install -D vitest
YARN
yarn add -D vitest
PNPM
pnpm add -D vitest

Vitest 配置
安裝完 Vitest 后,需要將其添加到 vite.config.js 文件中:
vite.config.js
import { defineConfig } from "vite";import vue from "@vitejs/plugin-vue";export default defineConfig({ plugins: [vue()], //add test to vite config test: { // ... },});
為 TypeScript 配置 Vitest 是類似的,但如果從 Vite 導(dǎo)入 defineConfig,我們需要在配置文件的頂部使用三斜線命令添加對(duì) Vitest 類型的引用。
/// <reference types="vitest" />import { defineConfig } from "vite";import vue from "@vitejs/plugin-vue";// https://vitejs.dev/config/export default defineConfig({ plugins: [vue()], test: { // ... },});
值得注意的是,Vitest 也可以在項(xiàng)目中通過在根文件夾中添加 vitest.config.js 文件來配置。如果這個(gè)文件存在,它將優(yōu)先于 vite.config.js 來配置Vitest。Vitest 也允許額外的配置,可以在配置頁面中找到。
事例演示:Notification
為了看看Vitest的運(yùn)作情況,我們創(chuàng)建一個(gè)顯示三種類型通知的通知組件:info、error 和success。這個(gè)組件的每個(gè)狀態(tài)如下所示:
notification.vue 內(nèi)容如下:
<template> <div :class="[ 'notification', type === 'error' ? 'notification--error' : null, type === 'success' ? 'notification--success' : null, type === 'info' ? 'notification--info' : null, message && message.length > 0 ? 'notification--slide' : null, ]" > <img src="https://res.cloudinary.com/djalafcj9/image/upload/v1634261166/getequityV2/denied_sbmv0e.png" v-if="type === 'error'" /> <img src="https://res.cloudinary.com/djalafcj9/image/upload/v1656690265/getequityV2/Frame_irxz3e.png" v-if="type === 'success'" /> <img src="https://res.cloudinary.com/djalafcj9/image/upload/v1634261166/getequityV2/pending_ctj1ke.png" v-if="type === 'info'" /> <p class="notification__text"> {{ message }} </p> <button ref="closeButton" class="notification__button" @click="$emit('clear-notification')" > <img src="https://res.cloudinary.com/djalafcj9/image/upload/v1635485821/getequityV2/close_muxdyb.png" /> </button> </div></template><script> export default { name: "Notification", emits: ['clear-notification'], props: { type: { type: String, default: null, }, message: { type: String, default: null, }, }, };</script><style> .notification { transition: all 900ms ease-out; opacity: 0; z-index: 300001; transform: translateY(-100vh); box-sizing: border-box; padding: 10px 15px; width: 100%; max-width: 730px; /* margin: 0 auto; */ display: flex; position: fixed; /* left: 0; */ top: 20px; right: 15px; justify-content: flex-start; align-items: center; border-radius: 8px; min-height: 48px; box-sizing: border-box; color: #fff; } .notification--slide { transform: translateY(0px); opacity: 1; } .notification--error { background-color: #fdecec; } .notification__text { margin: 0; margin-left: 17px; margin-right: auto; } .notification--error .notification__text { color: #f03d3e; } .notification--success { background-color: #e1f9f2; } .notification--success > .notification__text { color: #146354; } .notification--info { background-color: #ffb647; } .notification__button { border: 0; background-color: transparent; }</style>
在這里,我們使用 message prop創(chuàng)建了一個(gè)顯示動(dòng)態(tài)消息的組件。我們還利用 type prop 來設(shè)計(jì)這個(gè)組件的背景和文本,并利用這個(gè) type prop 顯示我們計(jì)劃的不同圖標(biāo)(error, success, info)。
最后,我們有一個(gè)按鈕,用來通過發(fā)出一個(gè)自定義事件:clear-notification來解除通知。
我們應(yīng)該測(cè)試什么?
現(xiàn)在我們對(duì)需要測(cè)試的組件的結(jié)構(gòu)有了了解,我們可以再思考一下,這個(gè)組件需要做什么,以達(dá)到預(yù)期的功能。
我們的測(cè)試需要檢查以下內(nèi)容:
- 該組件根據(jù)通知類型渲染出正確的樣式。
- 當(dāng) message 為空時(shí),通知就會(huì)逐漸消失。
- 當(dāng)關(guān)閉按鈕被點(diǎn)擊時(shí),該組件會(huì)發(fā)出一個(gè)事件。
為了測(cè)試這些功能,在項(xiàng)目中添加一個(gè) notification.test.js 用于測(cè)試。
安裝測(cè)試依賴項(xiàng)
在編寫單元測(cè)試時(shí),可能會(huì)有這樣的情況:我們需要用一個(gè)什么都不做的假組件來替換組件的現(xiàn)有實(shí)現(xiàn)。這被稱為 **stub(存根)**,為了在測(cè)試中使用存根,我們需要訪問Vue Test Utils的mount方法,這是Vue.js的官方測(cè)試工具庫。
現(xiàn)在我們來安裝Vue Test Utils。
安裝
npm install --save-dev @vue/test-utils@next# oryarn add --dev @vue/test-utils@next
現(xiàn)在,在我們的測(cè)試文件中,我們可以從"@vue/test-utils"導(dǎo)入 mount。
notification.test.js
import { mount } from "@vue/test-utils";
在測(cè)試中,我們還需要能夠模擬 DOM。Vitest目前同時(shí)支持 hAppy-dom 和 jsdom。對(duì)于這個(gè)演示,我們將使用happy-dom,然后安裝它:
yarn add happy-dom --dev
安裝后,我們可以在測(cè)試文件的頂部添加以下注釋...
notification.test.js
/** * @vitest-environment happy-dom */
.或者將此添加到 vite/vitest 配置文件中,以避免在有多個(gè)需要 happy-dom 工作的測(cè)試文件時(shí)出現(xiàn)重復(fù)情況。
vite.config.js
import { defineConfig } from "vite";import vue from "@vitejs/plugin-vue";// https://vitejs.dev/config/export default defineConfig({ plugins: [vue()], test: { environment: "happy-dom", },});
因?yàn)槲覀冎挥幸粋€(gè)測(cè)試文件,所以我們可以選擇第一個(gè)選項(xiàng),所以我們測(cè)試文件內(nèi)容如下:
notification.test.js
/** * @vitest-environment happy-dom */import { mount } from "@vue/test-utils";
有了這些依賴關(guān)系,我們現(xiàn)在可以導(dǎo)入我們要測(cè)試的組件。
notification.test.js
/** * @vitest-environment happy-dom */import { mount } from "@vue/test-utils";import notification from "../components/notification.vue";
常見的Vitest方法
為了編寫測(cè)試,我們需要利用以下常見的方法,這些方法可以從 Vitest 導(dǎo)入。
- describe:這個(gè)函數(shù)接受一個(gè)名字和一個(gè)函數(shù),用于將相關(guān)的測(cè)試組合在一起。當(dāng)你為一個(gè)有多個(gè)測(cè)試點(diǎn)(如邏輯和外觀)的組件編寫測(cè)試時(shí),它就會(huì)很方便。
- test/it:這個(gè)函數(shù)代表被測(cè)試的實(shí)際代碼塊。它接受一個(gè)字符串,通常是測(cè)試案例的名稱或描述(例如,渲染成功的正確樣式)和另一個(gè)函數(shù),所有的檢查和測(cè)試在這里進(jìn)行。
- expect:這個(gè)函數(shù)用于測(cè)試值或創(chuàng)建斷言。它接受一個(gè)預(yù)期為實(shí)際值(字符串、數(shù)字、對(duì)象等)的參數(shù)x,并使用任何支持的方法對(duì)其進(jìn)行評(píng)估(例如toEqual(y),檢查 x 是否與 y 相同)。
因此,我們現(xiàn)在將這些導(dǎo)入我們的測(cè)試文件中
notification.test.js
/** * @vitest-environment happy-dom */import { mount } from "@vue/test-utils";import notification from "../components/notification.vue";import { describe, expect, test } from "vitest";
有了這些函數(shù),我們開始構(gòu)建我們的單元測(cè)試。
建立 Vitest 單元測(cè)試
首先使用 describe 方法將測(cè)試分組。
notification.test.js
describe("notification.vue", () => { });
在 describe 塊內(nèi),我們添加每個(gè)實(shí)際的測(cè)試。
我們第一個(gè)要測(cè)試的用例是:組件根據(jù)通知類型渲染出正確的樣式。
notification.test.js
describe("notification.vue", () => { test("renders the correct style for error", () => { });});
renders the correct style for error 表示 test 所檢查的內(nèi)容的 name。它有助于為代碼塊檢查的內(nèi)容提供上下文,這樣就可以由原作者以外的人輕松維護(hù)和更新。它也使人們?nèi)菀鬃R(shí)別一個(gè)特定的失敗的測(cè)試案例。

notification.test.js
describe("notification.vue", () => { test("renders the correct style for error", () => { const type = "error"; });});
在我們組件中,定義了一個(gè) type 參數(shù),它接受一個(gè)字符串,用來決定諸如背景顏色、圖標(biāo)類型和文本顏色在組件上的渲染。在這里,我們創(chuàng)建一個(gè)變量 type,并將我們正在處理的類型之一,error (error, info, 或 success)分配給它。
notification.test.js
describe("notification.vue", () => { test("renders the correct style for error", () => { const type = "error"; const wrapper = mount(notification, { props: { type }, }); });});
在這里,我們使用 mount 來存根我們的組件,以便進(jìn)行測(cè)試。
mount 接受組件作為第一個(gè)參數(shù),接受一個(gè)選項(xiàng)列表作為第二個(gè)參數(shù)。這些選項(xiàng)提供了不同的屬性,目的是確保你的組件能在瀏覽器中正常工作。
在這個(gè)列表中,我們只需要 props 屬性。我們使用這個(gè)屬性是因?yàn)槲覀兊?nbsp;notification.vue組件至少需要一個(gè) prop 才能有效工作。
notification.test.js
describe("notification.vue", () => { test("renders the correct style for error", () => { const type = "error"; const wrapper = mount(notification, { props: { type }, }); expect(wrapper.classes()).toEqual( expect.arrayContaining(["notification--error"]) ); });});
在這一點(diǎn)上,剩下的就是寫一個(gè)斷言,或者更好的是,寫出我們組件的預(yù)期行為,即:renders the correct style for error。
為了做到這一點(diǎn),我們使用了 expect 方法。它接受我們的存根組件和所有的選項(xiàng)(在我們的例子中,我們把它命名為wrapper以方便參考)。
這個(gè)方法可以被鏈接到其他一些方法上,但是對(duì)于這個(gè)特定的斷言,我們要重新檢查組件的類列表是否返回一個(gè)包含這個(gè) notification——error 的數(shù)組。。
我們使用 classes 函數(shù)來實(shí)現(xiàn)這一點(diǎn),該函數(shù)返回包含該組件所有類的數(shù)組。在這之后,下一件事就是使用 toEqual 函數(shù)進(jìn)行比較,它檢查一個(gè)值 X 是否等于** Y**。在這個(gè)函數(shù)中,我們檢查它是否返回一個(gè)包含我們的類的數(shù)組: notification--error。
同樣,對(duì)于 type 為 success 或 info 類型,測(cè)試過程也差不多。
import { mount } from "@vue/test-utils";import notification from "../components/notification.vue";import { describe, expect, test } from "vitest";describe("notification.vue", () => { test("renders correct style for error", () => { const type = "error"; const wrapper = mount(notification, { props: { type }, }); expect(wrapper.classes()).toEqual( expect.arrayContaining(["notification--error"]) ); }); test("renders correct style for success", () => { const type = "success"; const wrapper = mount(notification, { props: { type }, }); expect(wrapper.classes()).toEqual( expect.arrayContaining(["notification--success"]) ); }); test("renders correct style for info", () => { const type = "info"; const wrapper = mount(notification, { props: { type }, }); expect(wrapper.classes()).toEqual( expect.arrayContaining(["notification--info"]) ); }); test("slides down when message is not empty", () => { const message = "success"; const wrapper = mount(notification, { props: { message }, }); expect(wrapper.classes()).toEqual( expect.arrayContaining(["notification--slide"]) ); });});
到這,我們已經(jīng)寫好了測(cè)試,以確保我們的通知是根據(jù)其類型來進(jìn)行樣式設(shè)計(jì)的。當(dāng)用戶點(diǎn)擊組件上的關(guān)閉按鈕時(shí),我們會(huì)重置 message 參數(shù)。根據(jù)我們的代碼,我們要根據(jù)這個(gè) message 參數(shù)的值來添加或刪除 notification--slide 類,如下所示:
notification.vue
<div :class="[ 'notification', type === 'error' ? 'notification--error' : null, type === 'success' ? 'notification--success' : null, type === 'info' ? 'notification--info' : null, message && message.length > 0 ? 'notification--slide' : null, ]" >//...
如果我們要測(cè)試這個(gè)特定的斷言,它的內(nèi)容如下:
test("slides up when message is empty", () => { const message = ""; const wrapper = mount(notification, { props: { message }, }); expect(wrapper.classes("notification--slide")).toBe(false); });
在這段測(cè)試代碼中,我們用一個(gè)空字符串創(chuàng)建一個(gè) message 變量,并把它作為一個(gè) prop 傳遞給我們的組件。
之后,我們檢查我們組件的類數(shù)組,確保它不包括 notification--slide 類,該類負(fù)責(zé)使我們的組件向下/向外滑動(dòng)到用戶的視圖。為了做到這一點(diǎn),我們使用 toBe 函數(shù),它接收一個(gè)值A,并試圖檢查它是否與 B 相同。
我們還想測(cè)試一下,每當(dāng)組件上的按鈕被點(diǎn)擊,它就會(huì)發(fā)出一個(gè)事件:
test("emits event when close button is clicked", async() => { const wrapper = mount(notification, { data() { return { clicked: false, }; }, }); const closeButton = wrapper.find("button"); await closeButton.trigger("click"); expect(wrapper.emitted()).toHaveProperty("clear-notification"); });
在這個(gè)測(cè)試塊中,我們使用了一個(gè) async 函數(shù),因?yàn)槲覀儗⒂|發(fā)一個(gè)事件,它返回一個(gè) Promise,我們需要等待這個(gè) Promise 的解決,以便捕捉這個(gè)事件所引起的變化。我們還使用了data函數(shù),并添加了一個(gè) clicked 屬性,當(dāng)點(diǎn)擊時(shí)將被切換。
到這,我們需要觸發(fā)這個(gè)點(diǎn)擊事件,我們首先通過使用 find 函數(shù)來獲得按鈕。這個(gè)函數(shù)與querySelector相同,它接受一個(gè)類、一個(gè)id或一個(gè)屬性,并返回一個(gè)元素。
在找到按鈕后,使用 trigger 方法來觸發(fā)一個(gè)點(diǎn)擊事件。這個(gè)方法接受要觸發(fā)的事件名稱(click, focus, blur, keydown等),執(zhí)行這個(gè)事件并返回一個(gè) promise。出于這個(gè)原因,我們等待這個(gè)動(dòng)作,以確保在我們根據(jù)這個(gè)事件做出斷言之前,已經(jīng)對(duì)我們的DOM進(jìn)行了改變。
最后,我們使用返回一個(gè)數(shù)組的 [emitted](
https://test-utils.vuejs.org/api/#emitted) 方法檢查我們的組件所發(fā)出的事件列表。然后我們檢查這個(gè)數(shù)組是否包括 clear-notification 事件。
最后,我們測(cè)試以確保我們的組件渲染出正確的消息,并傳遞給 message prop。
test("renders message when message is not empty", () => { const message = "Something happened, try again"; const wrapper = mount(notification, { props: { message }, }); expect(wrapper.find("p").text()).toBe(message); });
這里,我們創(chuàng)建了一個(gè) message 變量,給它分配了一個(gè)隨機(jī)字符串,并把它作為一個(gè) prop 傳遞給我們的組件。
然后,我們使用 p 標(biāo)簽搜索我們的消息文本,因?yàn)檫@里是顯示消息的地方,并檢查其文本是否與 message 相同。
我們使用 text 方法提取這個(gè)標(biāo)簽的內(nèi)容,這和 innerText很相似。最后,我們使用前面的函數(shù) toBe 來斷言這個(gè)值與 message 相同。
完整的測(cè)試文件
在涵蓋所有這些之后,下面是完整的測(cè)試文件內(nèi)容:
notification.test.js
/** * @vitest-environment happy-dom */import { mount } from "@vue/test-utils";import notification from "../components/notification.vue";import { describe, expect, test } from "vitest";describe("notification.vue", () => { test("renders the correct style for error", () => { const type = "error"; const wrapper = mount(notification, { props: { type }, }); expect(wrapper.classes()).toEqual( expect.arrayContaining(["notification--error"]) ); }); test("renders the correct style for success", () => { const type = "success"; const wrapper = mount(notification, { props: { type }, }); expect(wrapper.classes()).toEqual( expect.arrayContaining(["notification--success"]) ); }); test("renders the correct style for info", () => { const type = "info"; const wrapper = mount(notification, { props: { type }, }); expect(wrapper.classes()).toEqual( expect.arrayContaining(["notification--info"]) ); }); test("slides down when message is not empty", () => { const message = "success"; const wrapper = mount(notification, { props: { message }, }); expect(wrapper.classes()).toEqual( expect.arrayContaining(["notification--slide"]) ); }); test("slides up when message is empty", () => { const message = ""; const wrapper = mount(notification, { props: { message }, }); expect(wrapper.classes("notification--slide")).toBe(false); }); test("emits event when close button is clicked", async() => { const wrapper = mount(notification, { data() { return { clicked: false, }; }, }); const closeButton = wrapper.find("button"); await closeButton.trigger("click"); expect(wrapper.emitted()).toHaveProperty("clear-notificatioon"); }); test("renders message when message is not empty", () => { const message = "Something happened, try again"; const wrapper = mount(notification, { props: { message }, }); expect(wrapper.find("p").text()).toBe(message); });});
有幾件事需要注意:
- 我們利用 mount 來存根我們要測(cè)試的組件,它是由Vue Test Utils提供的。(yarn add --dev @vue/test-utils@next)
##運(yùn)行測(cè)試
現(xiàn)在已經(jīng)完成了測(cè)試的編寫,需要運(yùn)行它們。要實(shí)現(xiàn)這一點(diǎn),我們?nèi)?nbsp;package.json,在我們的scripts 部分添加以下幾行。
"scripts": { "test": "vitest", "coverage": "vitest run --coverage"},
如果在終端運(yùn)行 yarn vitest 或 yarn test,我們的測(cè)試文件就會(huì)被運(yùn)行,我們應(yīng)該看到測(cè)試結(jié)果和故障。

到這,我們已經(jīng)成功地使用Vitest運(yùn)行了我們的第一次測(cè)試。從結(jié)果中需要注意的一點(diǎn)是,由于Vitest的智能和即時(shí)觀察模式,這個(gè)命令只需要運(yùn)行一次,并在我們對(duì)測(cè)試文件進(jìn)行更新和修改時(shí)被重新運(yùn)行。
總結(jié)
使用 Vitest 對(duì)我們的應(yīng)用程序進(jìn)行單元測(cè)試是無縫的,與Jest等替代品相比,需要更少的步驟來啟動(dòng)和運(yùn)行。Vitest 還可以很容易地將現(xiàn)有的測(cè)試從 Jest 遷移到Vitest,而不需要進(jìn)行額外的配置。
作者:Timi Omoyeni 譯者:前端小智 來源:vuemastery 原文:
https://www.vuemastery.com/blog/getting-started-with-vitest