跳轉到內容

測試

為什麼需要測試?

自動化測試可以幫助您和您的團隊快速、自信地構建複雜的Vue應用程式,透過防止迴歸並鼓勵您將應用程式分解為可測試的函式、模組、類和元件來實現。與任何應用程式一樣,您的新Vue應用程式可能會以多種方式出現故障,因此在釋出之前能夠捕捉到這些問題並修復它們非常重要。

在本指南中,我們將介紹基本術語,並提供我們對Vue 3應用程式選擇哪些工具的建議。

有一個Vue特定的部分涉及可組合性。有關更多詳細資訊,請參閱下面的測試可組合性

何時進行測試

儘早開始測試!我們建議您一有機會就開始編寫測試。您等待新增測試到應用程式的時間越長,應用程式的依賴性就越多,開始測試就越困難。

測試型別

在設計您的Vue應用程式的測試策略時,您應該利用以下測試型別

  • 單元:檢查給定函式、類或可組合性的輸入是否產生預期的輸出或副作用。
  • 元件:檢查元件是否掛載、渲染、可互動以及按預期行為。這些測試比單元測試匯入的程式碼更多,更復雜,執行時間更長。
  • 端到端測試:檢查跨越多個頁面的功能,並對您生產的Vue應用程式進行實際網路請求。這些測試通常需要啟動資料庫或其他後端。

每種測試型別都在您的應用程式測試策略中扮演著角色,並且每種都能幫助您防止不同型別的問題。

概述

我們將簡要討論這些測試是什麼,如何在Vue應用程式中實現它們,並提供一些一般性建議。

單元測試

單元測試的目的是驗證小而獨立的程式碼單元是否按預期工作。單元測試通常涵蓋一個單獨的函式、類、可組合式或模組。單元測試側重於邏輯正確性,並且只關注應用程式整體功能的一小部分。它們可能會模擬應用程式環境的大部分內容(例如,初始狀態、複雜的類、第三方模組和網路請求)。

一般來說,單元測試將捕獲函式的業務邏輯和邏輯正確性問題。

以這個 increment 函式為例

js
// helpers.js
export function increment (current, max = 10) {
  if (current < max) {
    return current + 1
  }
  return current
}

因為它非常獨立,所以呼叫 increment 函式並斷言它返回它應該返回的內容將非常容易,因此我們將編寫一個單元測試。

如果這些斷言中的任何一個失敗,那麼很清楚問題就包含在 increment 函式內部。

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)
  })
})

如前所述,單元測試通常應用於獨立業務邏輯、元件、類、模組或函式,這些不涉及UI渲染、網路請求或其他環境問題。

這些通常是與Vue無關的純JavaScript / TypeScript模組。一般來說,在Vue應用程式中編寫業務邏輯的單元測試與其他框架使用應用程式的單元測試沒有顯著差異。

有兩個情況您確實需要進行Vue特定功能的單元測試

  1. 可組合式
  2. 元件

可組合式

Vue應用程式中特定的一類函式是可組合式,在測試過程中可能需要特殊處理。有關更多詳細資訊,請參閱下文的測試可組合式

元件單元測試

元件可以透過兩種方式來測試

  1. 白盒:單元測試

    “白盒測試”是瞭解元件的實現細節和依賴關係的測試。它們專注於隔離正在測試的元件。這些測試通常會模擬元件的一些或所有子元件,以及設定外掛狀態和依賴關係(例如Pinia)。

  2. 黑盒:元件測試

    “黑盒測試”不瞭解元件的實現細節。這些測試儘可能模擬最少的內容來測試元件與整個系統的整合。它們通常渲染所有子元件,被視為更“整合測試”。請參閱下文的元件測試建議

建議

  • Vitest

    由於由 create-vue 建立的官方設定基於 Vite,我們建議使用能夠直接利用 Vite 的相同配置和轉換管道的單元測試框架。專為這一目的設計的單元測試框架 Vitest 由 Vue / Vite 團隊成員建立和維護。它與基於 Vite 的專案整合簡單,且執行速度極快。

其他選項

  • Jest 是一個流行的單元測試框架。然而,我們只推薦 Jest 如果您有一個現有的 Jest 測試套件需要遷移到基於 Vite 的專案,因為 Vitest 提供了更順暢的整合和更好的效能。

元件測試

在 Vue 應用中,元件是 UI 的主要構建塊。因此,元件是驗證應用程式行為時自然隔離的單位。從粒度角度來看,元件測試位於單元測試之上,可以被視為一種整合測試。您的 Vue 應用程式的大部分內容應該由元件測試覆蓋,並且我們建議每個 Vue 元件都有自己的 spec 檔案。

元件測試應捕獲與元件的 props、它提供的 events、slots、styles、classes、生命週期鉤子等相關的問題。

元件測試不應模擬子元件,而應透過以使用者的方式進行互動來測試元件與其子元件之間的互動。例如,元件測試應像使用者一樣點選元素,而不是以程式設計方式與元件互動。

元件測試應專注於元件的公共介面,而不是內部實現細節。對於大多陣列件,公共介面僅限於:發出的事件、props 和 slots。在測試時,請記住 測試元件做什麼,而不是它是如何做的

DO

  • 對於 視覺 邏輯:根據輸入的 props 和 slots 斷言正確的渲染輸出。

  • 對於 行為 邏輯:斷言在使用者輸入事件響應下正確的渲染更新或發出的事件。

    在下面的示例中,我們展示了一個 Stepper 元件,該元件有一個標記為 "increment" 的 DOM 元素,可以被點選。我們傳遞一個名為 max 的 prop,該 prop 防止 Stepper 超過 2,所以如果我們點選按鈕 3 次,UI 仍然顯示 2

    我們對 Stepper 的實現一無所知,只知道 "input" 是 max prop,而 "output" 是使用者將看到的 DOM 狀態。

Vue Test Utils
Cypress
Testing Library
js
const valueSelector = '[data-testid=stepper-value]'
const buttonSelector = '[data-testid=increment]'

const wrapper = mount(Stepper, {
  props: {
    max: 1
  }
})

expect(wrapper.find(valueSelector).text()).toContain('0')

await wrapper.find(buttonSelector).trigger('click')

expect(wrapper.find(valueSelector).text()).toContain('1')

DON'T

  • 不要斷言元件例項的私有狀態或測試元件的私有方法。測試實現細節會使測試變得脆弱,因為它們更有可能在實現更改時崩潰並需要更新。

    元件的最終任務是渲染正確的 DOM 輸出,因此專注於 DOM 輸出的測試提供了相同的正確性保證(如果不是更多),同時更穩健且對變化更具彈性。

    不要完全依賴快照測試。斷言 HTML 字串並不能描述正確性。編寫有目的性的測試。

    如果一個方法需要徹底測試,考慮將其提取為一個獨立的實用函式,併為它編寫專門的單元測試。如果無法乾淨地提取,它可以作為元件、整合或端到端測試的一部分進行測試,該測試涵蓋了它。

建議

Vitest與基於瀏覽器的執行程式之間的主要區別是速度和執行上下文。簡而言之,基於瀏覽器的執行程式,如Cypress,可以捕捉到基於節點的執行程式(如Vitest)無法捕獲的問題(例如,樣式問題、真實的原生DOM事件、cookie、本地儲存和網路故障),但基於瀏覽器的執行程式比Vitest慢得多,因為它們確實打開了瀏覽器、編譯了您的樣式表等等。Cypress是一個支援元件測試的基於瀏覽器的執行程式。請閱讀 Vitest的比較頁面 瞭解關於Vitest和Cypress的最新資訊。

掛載庫

元件測試通常涉及將正在測試的元件單獨掛載,觸發模擬的使用者輸入事件,並斷言渲染的DOM輸出。有一些專門的實用庫可以簡化這些任務。

  • @vue/test-utils 是官方的低階元件測試庫,它被編寫來為使用者提供訪問Vue特定API的許可權。它也是 @testing-library/vue 所構建在之上的低階庫。

  • @testing-library/vue 是一個Vue測試庫,專注於在不依賴實現細節的情況下測試元件。其指導原則是,測試越接近軟體的使用方式,它們可以提供的信心就越大。

我們建議在應用程式中測試元件時使用 @vue/test-utils。由於 @testing-library/vue 在測試使用Suspense的非同步元件時存在問題,因此應謹慎使用。

其他選項

  • Nightwatch 是一個具有Vue元件測試支援的端到端測試執行程式。(示例專案

  • WebdriverIO 用於基於標準化自動化的跨瀏覽器元件測試,它依賴於原生使用者互動。它也可以與Testing Library一起使用。

端到端測試

雖然單元測試可以為開發者提供一定程度的信心,但當部署到生產環境時,單元和元件測試在提供對應用程式的整體覆蓋方面的能力有限。因此,端到端(E2E)測試提供了對應用程式最關鍵方面的覆蓋:當用戶實際使用您的應用程式時會發生什麼。

端到端測試關注於多頁應用行為,這些行為會針對您生產構建的Vue應用程式發起網路請求。它們通常涉及啟動資料庫或其他後端,甚至可以在即時預釋出環境中執行。

端到端測試通常會捕獲與您的路由、狀態管理庫、頂級元件(例如App或Layout)、公共資產或任何請求處理相關的錯誤。如上所述,它們可以捕獲單元測試或元件測試難以發現的關鍵錯誤。

端到端測試不匯入Vue應用程式的任何程式碼,而是完全透過在真實瀏覽器中導航整個頁面來測試您的應用程式。

端到端測試驗證了您應用程式中的多個層級。它們可以針對您本地構建的應用程式,甚至是一個即時預釋出環境。針對預釋出環境的測試不僅包括您的前端程式碼和靜態伺服器,還包括所有相關的後端服務和基礎設施。

您的測試越接近您軟體的使用方式,它們就能給您帶來越多的信心。 —— Kent C. Dodds —— Testing Library的作者

透過測試使用者操作如何影響您的應用程式,端到端測試通常是判斷應用程式是否正常工作的重要關鍵。

選擇端到端測試解決方案

儘管在網路上進行的端到端(E2E)測試因其不可靠(易出故障)的測試和減緩開發流程而獲得了負面聲譽,但現代E2E工具已經取得了長足的進步,以建立更可靠、互動和有用的測試。在選擇E2E測試框架時,以下部分提供了一些在選擇適用於您應用程式的測試框架時應考慮的指導。

跨瀏覽器測試

端到端(E2E)測試廣為人知的主要好處之一是能夠跨多個瀏覽器測試您的應用程式。雖然擁有100%的跨瀏覽器覆蓋率可能看起來很有吸引力,但需要注意的是,由於執行它們所需的額外時間和機器功率,跨瀏覽器測試在團隊資源上的回報是遞減的。因此,在確定您的應用程式需要的跨瀏覽器測試量時,應謹慎考慮這種權衡。

更快的反饋迴圈

端到端(E2E)測試和開發的一個主要問題是執行整個套件需要很長時間。通常,這僅在持續整合和持續部署(CI/CD)管道中完成。現代E2E測試框架透過新增諸如並行化等功能來幫助解決這個問題,這使得CI/CD管道往往比以前快得多。此外,在本地開發時,能夠選擇性地執行您正在工作的頁面的單個測試,同時提供測試的熱過載,可以幫助提高開發者的工作流程和生產力。

一流的除錯體驗

雖然開發者傳統上依賴於在終端視窗中掃描日誌來確定測試中發生了什麼錯誤,但現代端到端(E2E)測試框架允許開發者利用他們已經熟悉的工具,例如瀏覽器開發者工具。

無頭模式下的可見性

當在持續整合/部署管道中執行端到端(E2E)測試時,它們通常在無頭瀏覽器中執行(即,不會為使用者開啟可見的瀏覽器)。現代E2E測試框架的關鍵特性是能夠在測試期間檢視應用程式的快照和/或影片,從而為錯誤發生的原因提供一些洞察。歷史上,維護這些整合相當繁瑣。

建議

  • Playwright 是一款優秀的E2E測試解決方案,支援Chromium、WebKit和Firefox。在Windows、Linux和macOS上本地或CI上進行測試,無頭或帶頭,具有谷歌Chrome的Android原生移動模擬和Mobile Safari。它具有資訊豐富的UI、出色的除錯能力、內建斷言、並行化、跟蹤,並旨在消除易變測試。支援 元件測試,但標記為實驗性。Playwright是開源的,由微軟維護。

  • Cypress 具有資訊豐富的圖形介面、出色的除錯能力、內建斷言、存根、抗抖動和快照。如前所述,它提供穩定的 元件測試 支援。Cypress支援基於Chromium的瀏覽器、Firefox和Electron。WebKit支援可用,但標記為實驗性。Cypress採用MIT許可證,但某些功能(如並行化)需要訂閱Cypress Cloud。

其他選項

  • Nightwatch 是一種基於 Selenium WebDriver 的E2E測試解決方案。這使它擁有最廣泛的瀏覽器支援範圍,包括原生移動測試。基於Selenium的解決方案將比Playwright或Cypress慢。

  • WebdriverIO 是基於WebDriver協議的Web和移動測試自動化框架。

食譜

將Vitest新增到專案中

在一個基於Vite的Vue專案中,執行

sh
> npm install -D vitest happy-dom @testing-library/vue

接下來,更新Vite配置以新增test選項塊

js
// vite.config.js
import { defineConfig } from 'vite'

export default defineConfig({
  // ...
  test: {
    // enable jest-like global test APIs
    globals: true,
    // simulate DOM with happy-dom
    // (requires installing happy-dom as a peer dependency)
    environment: 'happy-dom'
  }
})

提示

如果您使用TypeScript,請將vitest/globals新增到tsconfig.json中的types欄位。

json
// tsconfig.json

{
  "compilerOptions": {
    "types": ["vitest/globals"]
  }
}

然後,在專案根目錄中建立一個以*.test.js結尾的檔案。您可以將所有測試檔案放置在專案根目錄中的測試目錄中或放在原始檔旁邊的測試目錄中。Vitest將自動使用命名約定搜尋它們。

js
// MyComponent.test.js
import { render } from '@testing-library/vue'
import MyComponent from './MyComponent.vue'

test('it should work', () => {
  const { getByText } = render(MyComponent, {
    props: {
      /* ... */
    }
  })

  // assert output
  getByText('...')
})

最後,更新package.json以新增測試指令碼並執行它

json
{
  // ...
  "scripts": {
    "test": "vitest"
  }
}
sh
> npm test

測試組合式

本節假定您已閱讀組合式部分。

當涉及到測試組合式時,我們可以將它們分為兩類:不依賴於宿主元件例項的組合式,以及依賴於宿主元件例項的組合式。

當組合式使用以下API時,它依賴於宿主元件例項:

  • 生命週期鉤子
  • 提供 / 注入

如果組合式僅使用響應性API,則可以直接呼叫它並斷言其返回的狀態/方法進行測試

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)
})

依賴於生命週期鉤子或提供/注入的組合式需要包裝在宿主元件中進行測試。我們可以建立以下型別的輔助器

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()
})

對於更復雜的組合式,也可以透過使用元件測試技術針對包裝元件編寫測試來更容易地進行測試。

測試已載入