回到部落格

視覺化測試:UI 開發中最厲害的技巧

以更少的維護獲得更多信心

loading
Michael Shilman
@mshilman
上次更新

在 UI 開發中,確保所有東西看起來正確與確保其運作同樣重要。視覺化測試是影像快照測試,旨在解決這個問題。

然而,有點令人驚訝的是,它們也可以取代許多 UI 單元測試中最脆弱的部分:斷言 UI 的細節。在許多情況下,這可以完全取代單元測試,讓您能夠以更少的程式碼測試更多內容。

這篇文章涵蓋了

如果您仍然在對元件進行單元測試,請繼續閱讀以了解更好的 UI 開發方式。

Illustration of a simplified Storybook on the left, labeled "Write code". There's an arrow pointing right labeled "Detect bugs". On the right is that same simplified Storybook with highlighted stories in the sidebar, labeled "Visual test". There's an arrow pointing left labeled "Fix".

視覺化測試 101

在我們深入探討為什麼視覺化測試如此出色之前,它是什麼以及它是如何運作的?

視覺化測試是一種快照測試,它比較程式碼變更之前之後的 UI 元件影像快照。如果快照不符,則測試失敗。

  • 差異可能是預期的,因此必須更新基準(之前)影像
  • 或者差異是意外的,使用者應該去修復程式碼。

以下是該流程在實務中的樣子

A workflow diagram with 5 steps. 1) Baseline; Create a story and save a snapshot as the baseline. 2) Update; Update your component code. 3) Run visual test; Take a snapshot of your changes and compare with the baseline. 4) Accept or deny; Approve, if the change is intentional, or deny the change. 5) New baseline; When the test is accepted, the baseline is updated.

更少的程式碼,更好的測試

視覺化測試非常棒,但為什麼我們認為它是測試 UI 的一種根本上更好的方式?簡短的答案是,視覺化測試比單元測試更容易編寫和維護。同時,它們提供更多信心,因為它們測試更多內容。

考慮一個使用 React Testing Library (RTL) 的簡單範例,這是像 Jest 和 Vitest 這樣的測試執行器中最流行的單元測試元件方式。

// Button.test.js
import { render, screen } from '@testing-library/react';
import Button from './Button';

it('uses custom text for the button label', () => {
  render(<Button>Click me!</Button>);
  expect(screen.getByRole('button')).toHaveTextContent('Click me!');
})

此測試掛載了 Button 元件,然後斷言按鈕標籤的文字內容。像 Playwright CT 和 Cypress CT 這樣的工具也使用類似的語法和結構。

Storybook 的語法略有不同,但概念相同。以下是 RTL 範例的等效範例

// Button.stories.js
import Button from './Button';
export default { component: Button };

export const CustomText = {
  args: { children: 'Click me!' },
  play: async ({ canvasElement }) => {
    await expect(canvasElement).toHaveTextContent('Click me!') 
  },
};

這是在 Storybook 內部的樣子

Screenshot of Storybook, showing the example Button component story and its passing test

透過像這樣的測試,我們僅斷言一件事:按鈕的文字。

視覺化測試不僅斷言按鈕包含正確的文字,還斷言按鈕是藍色的、具有圓角、以相同的字體呈現等等。而且它們在沒有編寫任何明確斷言的情況下做到這一點。

以下是使用 Visual Tests 擴充套件後,測試變得多麼簡單

export const CustomText = {
  args: { children: 'Click me!' },
};

在以下範例中,我不小心在全球 CSS 中引入了一個錯誤,該錯誤剝奪了 Button 的大部分樣式。這將通過 RTL 的功能測試,但我們的視覺化測試會捕捉到差異並將其顯示為變更

Screenshot of Storybook showing the example Button story and its failing visual test

真實世界範例

節省一行斷言似乎沒什麼大不了的,但在真實世界的專案中,好處會迅速累積。考慮像 Mealdrop 的購物車這樣的元件

Screenshot of Storybook showing the ShoppingCartMenu component's "With Items" story

在功能上,我們想要測試購物車中的所有商品是否正確顯示,以及結帳按鈕是否已啟用,因為購物車中有商品。

透過視覺化測試,我們可以使用 story WithItems 來測試這一點,該 story 設定了購物車及其輸入,但實際上不包含任何明確的測試邏輯

// ShoppingCartMenu.stories.js
import { ShoppingCartMenu } from './ShoppingCartMenu'

export default { component: ShoppingCartMenu };

export const WithItems = {
  args: {
    cartItems: [ /* items */ ],
    totalPrice: 1200
  },
}

如果我們不相信已啟用的按鈕會在 UI 中呈現不同的樣子,我們可以擴充該測試以定義 WithItemsEnabled,該測試專門驗證按鈕未停用

// ShoppingCartMenu.stories.js

export const WithItemsEnabled = {
  ...WithItems,
  play: async ({ canvasElement }) => {
    const checkout = await findByRole(canvasElement, 'button');
    await expect(checkout).not.toBeDisabled();
  },
}

現在想像一下僅在 RTL 中編寫相同的測試。我們會想要測試購物車中的每個商品是否以正確的數量出現,總計是否正確等等。

// ShoppingCartMenu.test.js

it('renders correctly with items', () => {
  render(<ShoppingCartMenu cartItems={[ /* items */ ]} totalPrice={1200} />);
  
  const fries = await screen.findByText(/^Fries$/);
  expect(getByText(fries.parentElement, '€2.50')).toBeInTheDocument();
  // More assertions here

  const cheeseburger = await screen.findByText(/^Cheeseburger$/);
  expect(getByText(cheeseburger.parentElement, '€8.50')).toBeInTheDocument();
  // More assertions here
  
  /*
   *
   *
   * Dozens of lines omitted here,
   * for everybody's sanity.
   *
   *
   */

  const checkout = screen.getByRole('button');
  expect(checkout).not.toBeDisabled();
});

當然,我們可以透過輔助函數來縮短所有這些內容,以檢查每個購物車商品,但是當我們需要為像這樣的測試編寫和維護輔助函數時,我們已經失敗了。

現在將這個單一測試乘以整個應用程式,其中可能包含數百個各種複雜性的元件。維護這類測試是一場噩夢。

相反,為數百個元件編寫 story 並對其進行視覺化測試是可行的,而且世界上最好的前端團隊已經在這樣做了。

測試 UX,而不是實作細節

測試大師 Cory House 最近評論了某人關於「自動化測試就像在程式碼上澆灌混凝土」的觀點。上一節中的 RTL 程式碼正是人們在自動化測試中抱怨的「混凝土」。

為了避免混凝土,Cory 建議「測試 UX,而不是實作細節」。而測試 UX 正是視覺化測試給我們帶來的。更重要的是,視覺化快照比程式碼更容易維護:正如我們在上面看到的,當您的 story 以所需狀態呈現時,更新測試就像按下按鈕接受新的基準快照一樣容易。

由於 Storybook 也支援 RTL 動作和查詢,因此您擁有盡可能多的能力來測試所需的任何細節層級,以獲得對程式碼的信心。

Storybook 中的視覺化測試

在 Storybook,我們非常堅信視覺化測試,因此我們已將其作為一項一流功能包含在內。Storybook 的 Visual Test 擴充套件由Chromatic提供支援,Chromatic 是世界上最好的視覺化測試基礎設施。

Chromatic 透過比較程式碼更新前後的影像快照來識別變更,並突出顯示差異以供審查。它在雲端中以數十秒的速度並行執行數千個測試,跨越多個瀏覽器(Chrome、Safari、Firefox、Edge)、視窗大小、主題和 i18n 地區設定。

Workflow diagram with three steps. 1) Push code. 2) Detect UI changes. 3) Get PR checks

Chromatic 提供 PR 檢查,以指示何時存在與 PR 關聯的視覺變更。當測試失敗時,使用者可以點擊進入高效的 UI 以查看視覺變更。到目前為止,PR 檢查一直是使用 Chromatic 和其他類似視覺化測試服務的主要工作流程。

Storybook 的Visual Tests 擴充套件是此工作流程的一種全新且創新的轉變,將 Chromatic 的強大功能置於 Storybook 本身內部。這讓您可以在開發時按需執行視覺化測試,而無需推送程式碼、執行 CI 並等待一堆不相關的檢查。

這是一個很棒的工作流程。從您的元件工作坊中,現在可以

  1. 啟動視覺化測試
  2. 篩選側邊欄以突出顯示視覺差異
  3. 在 Storybook 內部查看並解決這些變更
Screenshot of a Storybook showing running visual tests and the highlighted test failures in the sidebar

Visual Tests 擴充套件使捕捉 UI 錯誤並在建構元件時保持流程比以往任何時候都更快。我們相信這是邁向 UI 開發「聖杯」的重要一步。

立即試用

Storybook 的 Visual Test 擴充套件包含在新的 Storybook 安裝中

npx storybook@latest init

如果您是從較舊版本的 Storybook 升級,您現在將被提示選擇是否要將擴充套件安裝到現有專案中

npx storybook@latest upgrade

下一步是什麼?

Visual Tests 擴充套件在今天的 Storybook 8 中是穩定且可用的。我們正在考慮以下增強功能

  1. 全螢幕檢視模式以接受和拒絕變更。
  2. 將測試範圍限定為目前可見的 story 或元件的能力。
  3. 始終開啟的「監看模式」,在您的開發機器上本地執行功能測試,並透過更快的意見回饋迴圈來補充視覺化測試。

如需我們正在考慮和積極進行的專案概述,請查看Storybook 的路線圖

加入 Storybook 電子郵件列表

獲取最新的新聞、更新和發布

6,730開發人員和計數中

我們正在招聘!

加入 Storybook 和 Chromatic 背後的團隊。建構被數十萬開發人員在生產環境中使用的工具。遠端優先。

查看職位

熱門文章

2023 年 JS 狀態:從猛烈的左鉤拳中反擊

Storybook 如何使用調查來引導開發
loading
Michael Shilman

Storybook 8.2

邁向不妥協的元件測試
loading
Michael Shilman

互動式 story 生成

在幾秒鐘內建立您的第一個 story,無需離開瀏覽器!
loading
Valentin Palkovic
加入社群
6,730開發人員和計數中
為什麼為什麼選擇 Storybook元件驅動的 UI
文件指南教學更新日誌遙測技術
社群擴充套件參與貢獻部落格
範例展示探索專案元件詞彙表
開放原始碼軟體
Storybook - Storybook 繁體中文

特別感謝 Netlify CircleCI