## 為什麼需要測試 自動化測試可以幫助你的團隊快速且自信地建立複雜的 Vue 專案,Vue 鼓勵你把你的專案切成可測試的函式、模組、類別與元件。因為就和任何新專案一樣,你的 Vue 專案可能以任何形式崩潰,所以測試就變得相當重要。(簡單來說就是可以利用測試確保的你的專案符合你的需求,或是幫助你找出 bug 等等) 這篇文章會介紹一些基本的技術並且提供可以使用在 Vue3 的推薦工具。 還有一個專門介紹 Vue 專屬的組合式函式測試方式的章節 ## 何時開始測試 越早越好,最好是專案建立時就能建立測試方法 ## 測試種類 當你在設計 Vue 的測試策略時,要考慮下面幾種測試 * 單元測試: 檢查給定的函式、類別或是組合式函式的輸出是否符合預期 * 元件測試: 檢查你的元件是否有正常掛載、渲染,並可以正常互動。 * End-to-end 測試: 檢查會跨頁面的功能,並且建立真實的網路請求,到了這個階段通常都會涉及後端或是資料庫 ### 單元測試 一般來說,單元測試會驗證函式的邏輯以及正確性 ```js= // helpers.js export function increment (current, max = 10) { if (current < max) { return current + 1 } return current } ``` ```js= // helpers.spec.js import { increment } from './helpers' describe('increment', () => { test('increments the current number by 1', () => { expect(increment(0, 10)).toBe(1) }) test('does not increment the current number over the max', () => { expect(increment(10, 10)).toBe(10) }) test('has a default max of 10', () => { expect(increment(10)).toBe(10) }) }) ``` 除了函式之外,在 Vue 中還有兩個東西需要單元測試 1. 組合式函式 2. 元件 #### 組合式函式測試 一般來說在測試組合式函式時,會分成兩個類別: 與元件有關聯的組合式函式和與元件無關的組合式函式。 如果組合式函式有用到以下 API ,表式它和元件有關: 1. Lifecycle hooks 2. Provide / Inject 如果只有使用到 Reactivity API,只要用 assert 就可以測試了(和一般的 unit test 一樣) ```js= // counter.js import { ref } from 'vue' export function useCounter() { const count = ref(0) const increment = () => count.value++ return { count, increment } } ``` ```js= // counter.test.js import { useCounter } from './counter.js' test('useCounter', () => { const { count, increment } = useCounter() expect(count.value).toBe(0) increment() expect(count.value).toBe(1) }) ``` 單如果牽扯到其他 API,或是使用了 Provide / Inject,它就必須被包在元件內來測試 ```js= // test-utils.js import { createApp } from 'vue' export function withSetup(composable) { let result const app = createApp({ setup() { result = composable() // suppress missing template warning return () => {} } }) app.mount(document.createElement('div')) // return the result and the app instance // for testing provide / unmount return [result, app] } ``` ```js= import { withSetup } from './test-utils' import { useFoo } from './foo' test('useFoo', () => { const [result, app] = withSetup(() => useFoo(123)) // mock provide for testing injections app.provide(...) // run assertions expect(result.foo.value).toBe(1) // trigger onUnmounted hook if needed app.unmount() }) ``` 等於說我們要建立並渲染一個測試用組件來測試我們的組合式函式。 如果要更複雜,包在元件裡面並使用元件測試會比較容易。 #### 元件的單元測試 一個元件可以用以下兩種方式來測試: 1. 白箱測試: 單元測試 這個測試會知道元件的細節以及和其他元件或函式的依賴關係,這裡更專注於元件的**獨立**測試,通常會涉及到模擬元件的部分子組件,以及設置的插件和其依賴狀態(例如 pinia) 3. 黑箱測試: 元件測試 這個測試就不會知道元件的細節或是依賴關係了,這邊就不太會做模擬,式用來測試元件在整個系統中的狀態,這裡會更像是整合測試(integration test) #### 推薦的工具 * [Vitest](https://vitest.dev/): Vitest 是由 Vite 開發的測試框架(畢竟 Vue 都推薦你使用 Vite 來建立專案了) * 其他: * [Peeky](https://peeky.dev/) * [Jest](https://jestjs.io/) ### 元件測試 因為在 Vue 中,元件是組成 UI 的基本架構,一般來說組件測試位於單元測試之上,可以被視為一種整合測試,所以你的測試應該要涵蓋到你 Vue 專案大部分功能,我們建議每個Vue組件都應該有自己的規格文件。 組件測試應該捕捉與你的組件相關的問題,包括組件的屬性(props)、事件(events)、提供的插槽(slots)、樣式、類別(classes)、生命周期鉤子等等。 元件測試不應該模擬子組件,而應該通過像使用者一樣與元件進行互動來測試元件與其子元件之間的互動。例如,元件測試應該像使用者一樣點擊元素,而不是以程式與組件進行互動。 元件測試應該專注於元件的介面,而不是內部實現細節。對於大多數元件來說,介面僅限於:發出的事件、屬性(props)和插槽。在進行測試時,請記住測試元件的功能,而不是它的實現方式。 #### 推薦的做法 對於視覺上的測試: 根據輸入的 prop 和插槽斷言渲染輸出是否正確。 對於互動上的測試: 斷言渲染的更新是否正確,或者觸發的事件是否正確地回應了用戶輸入事件。 官網提供的[例子](https://cn.vuejs.org/guide/scaling-up/testing.html#component-testing) #### 不推薦的做法 不要斷言元件的 private 方法或是 private 狀態,寫這麼細會使得你的測試程式碼太脆弱,可能你內部方法有個小地方更改你就必須整個測試重寫。 元件最終的目標就是渲染 DOM 元素,所以只要專注在這上面就好。 不要完全依賴快照(snapshot),斷言 HTML 字串並不能完全說明正確性 如果一個方法需要測試,那就把他單獨提取出來並為他專門寫一個獨立的單元測試,如果他不能直接抽出來那他就該被視為元件、整合或是 e2e 測試的一部份。 #### 推薦方案 * [Vitest](https://vitest.dev/),元件和 DOM 可以用 [@vue/test-utils](https://github.com/vuejs/test-utils) 做測試 * [Cypress](https://docs.cypress.io/guides/component-testing/overview) 與 [library](https://testing-library.com/docs/cypress-testing-library/intro/) 一起使用就可以測試 Vitest 與 其他基於瀏覽器運行的工具不同之處在於運行速度與執行的上下文。簡而言之,基於瀏覽器的運行器(例如 Cypress)能夠捕捉到基於 Node 的運行器(如 Vitest)所無法捕捉到的問題(如樣式問題、原生 DOM 事件、Cookies、本地儲存和網路故障),但基於瀏覽器的運行器比 Vitest 慢數倍,因為它們需要執行開啟瀏覽器、編譯樣式表等步驟。請閱讀 Vitest 文件中的[「比較」](https://vitest.dev/guide/comparisons.html#cypress)章節以了解 Vitest 和 Cypress 的差異。 #### 掛載的 library * [@vue/test-utils](https://github.com/vuejs/test-utils): 這是官方的低階元件測試庫,旨在讓用戶可以訪問Vue特定的API。 * [@testing-library/vue](https://github.com/testing-library/vue-testing-library): 這是一個專注於測試元件而不依賴於實現細節的測試庫。 我們建議在應用程式中使用 @vue/test-utils 來測試組件。在測試帶有 Suspense 的異步組件時,使用 @testing-library/vue 可能會遇到問題,因此在使用時需要謹慎考慮。 * 其他: * [Nightwatch](https://nightwatchjs.org/) * [WebdriverIO](https://webdriver.io/docs/component-testing/vue/) ### E2E 測試 E2E 測試的重點: 使用者實際在使用你的應用程式時到底會遇到什麼問題。通常,這些測試可以捕捉到與路由、狀態管理庫、頂級元件(通常是 App 或 Layout)、公共資源或任何請求處理相關的問題。如上所述,它們能夠捕捉到單元測試或元件測試無法捕捉到的關鍵問題。 #### 選擇一個 e2e 測試解決方案 * 跨瀏覽器測試 E2E 測試的主要優勢之一是能夠在多個瀏覽器上測試你的應用程序。儘管在各個瀏覽器上達到100%的覆蓋率似乎很理想,但需要注意的是,由於需要額外的時間和計算資源來保持一致地運行測試,跨瀏覽器測試對團隊資源的回報會逐漸減少。因此,在選擇應用程式所需的跨瀏覽器測試量時,必須在這種平衡方面保持警覺。 * 更快的回饋 e2e 測試與開發的主要問題之一是運行整個測試套件需要很長時間。通常,這僅在持續集成和部署(CI/CD)流程中執行。現代的 E2E 測試框架通過添加立如並行處理之類的功能來解決這個問題,使得CI/CD流程的運行速度往往比以前快了許多倍。此外,在本地開發時,能夠選擇性地運行正在工作的頁面的單個測試,同時還提供測試的熱重新加載,可以幫助提升開發者的工作流程和生產力。 * 一流的 debug 體驗 傳統上,開發人員經常依賴於在 terminal 中的 log,以幫助確定測試中出了什麼問題。然而,現代的 E2E 測試框架使開發人員能夠利用他們已經熟悉的工具,例如瀏覽器開發者工具。 * 無界面模式下的可見性 當 E2E 測試在 CI/CD 流程中運行時,它們通常在無界面瀏覽器中運行(即,用戶無法看到打開的瀏覽器)。現代 E2E 測試框架的一個重要功能是在測試期間查看應用程序的快照和/或影片,以了解為什麼會發生錯誤。 #### 推薦工具 * [Cypress](https://www.cypress.io/) * 其他 * [Playwright](https://playwright.dev/) * [Nightwatch](https://nightwatchjs.org/) * [WebdriverIO](https://webdriver.io/) ## 指南 安裝與設定 Vitest ```bash= npm install -D vitest happy-dom @testing-library/vue ``` ```javascript= // vite.config.js import { defineConfig } from 'vite' export default defineConfig({ // ... test: { // 启用类似 jest 的全局测试 API globals: true, // 使用 happy-dom 模拟 DOM // 这需要你安装 happy-dom 作为对等依赖(peer dependency) environment: 'happy-dom' } }) ``` * ts 的寫法不同 ```json= // tsconfig.json { "compilerOptions": { "types": ["vitest/globals"] } } ``` 撰寫測試文件 ```javascript= // MyComponent.test.js import { render } from '@testing-library/vue' import MyComponent from './MyComponent.vue' test('it should work', () => { const { getByText } = render(MyComponent, { props: { /* ... */ } }) // 断言输出 getByText('...') }) ``` 添加命令 ```json= { // ... "scripts": { "test": "vitest" } } ``` ```bash= npm test ``` ## 補充 [Storybook](https://storybook.js.org/) [軟體測試實務-1](https://www.tenlong.com.tw/products/9786263334854?list_name=c-software-testing) [軟體測試實務-2](https://www.tenlong.com.tw/products/9786263334861?list_name=c-software-testing)