返回至UI 測試手冊
React
章節
  • 簡介
  • 視覺化
  • 組合
  • 互動
  • 可訪問性
  • 使用者流程
  • 自動化
  • 工作流程
  • 結論

Storybook 中的視覺化測試

學習如何自動精確找出 UI 錯誤

要交付沒有錯誤的 UI 非常困難。過去,開發人員使用單元測試和快照測試來掃描 HTML 程式碼中的錯誤。但是這些方法無法呈現使用者實際看到的東西,因此錯誤始終無法消除。

視覺化測試透過擷取和比較真實瀏覽器中的圖片快照來捕捉錯誤。它讓您可以自動化檢查 UI 外觀是否正確的流程。

什麼是視覺化錯誤?

視覺化錯誤無處不在。元素被截斷、顏色或字體樣式不正確、版面配置損壞,以及缺少錯誤狀態。

現在每間公司都是軟體公司。這表示每間公司都有責任維護 UI。但如果您和我一樣,您可能會注意到公司似乎永遠沒有足夠的人員來隨時監控 UI 的每個部分。

視覺化錯誤是 UI 外觀中非故意的錯誤,使其看起來不可靠。它們是容易用肉眼看出來,但常見的測試方法無法捕捉到的回歸錯誤。

大多數測試旨在驗證邏輯,這是合理的:您執行一個函數,取得其輸出,並檢查其是否正確。電腦非常擅長驗證資料。但是外觀如何呢?

嗯,這個問題有兩個層面。

1. 外觀是否正確?

以這個 Task 元件為例。它的外觀會根據其所處的狀態而有所不同。我們顯示一個已選取(或未選取)的核取方塊、一些關於任務的資訊,以及一個釘選按鈕。當然,還有所有相關的樣式。

Different states of a task component

第一個挑戰只是驗證元件在所有這些情境中的外觀。這需要大量調整 props 和 state 以設定和測試每個案例。喔,而且電腦真的無法告訴您它是否符合規格。您,開發人員,必須目視檢查它。

2. 外觀仍然正確嗎?

您第一次建置時是正確的。它在所有狀態下看起來都不錯。但是變更會隨著開發的自然進程而發生。錯誤不可避免地會偷偷溜進來。對於介面來說尤其如此。一個小的 CSS 調整可能會破壞元件或其某個狀態。

您無法在每次進行變更時手動檢查 UI 的廣度。您需要更自動化的東西。

視覺化測試

視覺化測試讓您可以使用一個統一的工作流程來處理這兩項任務。它是驗證元件外觀的過程,無論是在您建置它的時候,還是在您迭代以交付新功能的時候。

以下是視覺化測試工作流程的外觀

  1. 🏷 隔離元件。使用 Storybook 專注於並一次測試一個元件。
  2. ✍🏽 寫出測試案例。 每個狀態都使用 props 和模擬資料重現。
  3. 🔍 手動驗證 每個測試案例的外觀。
  4. 📸 自動捕捉 UI 錯誤。 擷取每個測試案例的快照,並使用機器式差異比對來檢查回歸錯誤。

視覺化測試的重點是將 UI 與應用程式的其餘部分(資料、後端、API)隔離。這讓您可以個別觀察每個狀態。然後您可以手動抽查並自動回歸測試這些狀態。

讓我們詳細了解每個步驟。

1. 隔離元件

透過一次測試一個元件並為其每個狀態編寫測試案例,可以更輕鬆地精確找出錯誤。傳統方法是在首次使用元件的應用程式頁面上建置元件。這使得模擬和驗證所有這些狀態變得困難。有一個更好的方法 — Storybook。

Storybook 是用於隔離建置元件的業界標準。Twitter、Slack、Airbnb、Shopify、Stripe 和 Microsoft 都在使用它。它封裝為一個小型獨立工具,與您的應用程式並存,為您提供

  • 📥 一個沙箱,用於隔離呈現每個元件
  • 🔭 將其所有狀態視覺化為故事
  • 📑 為每個元件記錄 props 和使用指南
  • 🗃️ 一個所有元件的目錄,使探索更容易

讓我們回到 Task 元件。隔離它表示我們單獨載入和呈現這個元件本身。為此,我們需要 Storybook。

設定 Storybook

我們的專案已預先設定為使用 Storybook。設定檔位於 .storybook 資料夾中,所有必要的腳本都已新增至 package.json

我們可以從為 Task 元件建立故事檔案開始。這會在 Storybook 中註冊元件並新增一個預設測試案例。

複製
src/components/Task.stories.jsx
import Task from './Task';

export default {
  component: Task,
  title: 'Task',
  argTypes: {
    onArchiveTask: { action: 'onArchiveTask' },
    onTogglePinTask: { action: 'onTogglePinTask' },
    onEditTitle: { action: 'onEditTitle' },
  },
};

export const Default = {
  args: {
    task: {
      id: '1',
      title: 'Buy milk',
      state: 'TASK_INBOX',
    },
  },
};

最後,執行以下命令以在開發模式下啟動 Storybook。您應該會看到 Task 元件載入。

複製
yarn storybook

我們現在準備好寫出測試案例。

2. 寫出測試案例

在 Storybook 中,測試案例稱為故事。故事捕捉元件的特定狀態 — 瀏覽器中實際呈現的狀態。

Task 元件有三種狀態 — default、pinned 和 archived。我們將為每個狀態新增一個故事。

複製
src/components/Task.stories.jsx
import Task from './Task';

export default {
  component: Task,
  title: 'Task',
  argTypes: {
    onArchiveTask: { action: 'onArchiveTask' },
    onTogglePinTask: { action: 'onTogglePinTask' },
    onEditTitle: { action: 'onEditTitle' },
  },
};

export const Default = {
  args: {
    task: {
      id: '1',
      title: 'Buy milk',
      state: 'TASK_INBOX',
    },
  },
};

export const Pinned = {
  args: {
    task: {
      id: '2',
      title: 'QA dropdown',
      state: 'TASK_PINNED',
    },
  },
};

export const Archived = {
  args: {
    task: {
      id: '3',
      title: 'Write schema for account menu',
      state: 'TASK_ARCHIVED',
    },
  },
};

3. 驗證

驗證是評估元件在 Storybook 中的外觀。也就是說,它是否符合設計規格?

通常的開發工作流程是

  1. 編輯程式碼
  2. 取得元件的適當狀態
  3. 評估其外觀

然後重複整個週期,直到您驗證所有狀態。

透過為每個狀態編寫故事,您可以省略第二個步驟。您可以直接從編輯程式碼到驗證所有測試案例。因此,大幅加快整個流程。

寫出故事也浮現出如果您以更臨時的方式開發,您可能不會考慮到的情境。例如,如果使用者輸入非常長的任務會發生什麼事?讓我們新增該故事以找出答案。

複製
src/components/Task.stories.jsx
const longTitleString = `This task's name is absurdly large. In fact, I think if I keep going I might end up with content overflow. What will happen? The star that represents a pinned task could have text overlapping. The text could cut-off abruptly when it reaches the star. I hope not!`;

export const LongTitle = {
  args: {
    task: {
      id: '4',
      title: longTitleString,
      state: 'TASK_INBOX',
    },
  },
};

現在我們已經驗證了每個測試案例的外觀,我們可以繼續下一步。自動捕捉回歸錯誤。但首先,提交您的變更。

4. 自動捕捉回歸錯誤

Task 元件的外觀符合我們在所有用例中的預期。但是,我們如何確保一條遺漏的 CSS 行不會在未來破壞它?手動瀏覽整個元件目錄以進行每次變更是不切實際的。

這就是為什麼開發人員使用視覺回歸測試工具來自動檢查回歸錯誤。Auth0、Twilio、Adobe 和 Peloton 都使用 Chromatic(由 Storybook 團隊建置)。

在這一點上,我們知道元件處於良好狀態。Chromatic 將擷取每個故事的圖片快照 — 如同它在瀏覽器中顯示的那樣。然後,每當您進行變更時,都會擷取新的快照並與先前的快照進行比較。然後,您會審查發現的任何視覺差異,以判斷它們是故意的更新還是意外的錯誤。

設定 Chromatic

登入並建立新專案並取得您的專案權杖。

Chromatic 是專為 Storybook 建置的,無需任何設定。執行以下命令將觸發它擷取每個故事的快照(使用雲端瀏覽器)。

複製
npx chromatic --project-token=<project-token>

第一次執行將設定為基準,即起點。並且每個故事都有自己的基準。

執行測試

在每次提交時,都會擷取新的快照並與現有基準進行比較,以偵測 UI 變更。讓我們看看實際運作的檢查。

首先,對 UI 進行調整。我們將變更釘選圖示和文字樣式。更新 Task 元件,然後提交並重新執行 Chromatic。

複製
src/components/Task.jsx
import PropTypes from 'prop-types';

export default function Task({
  task: { id, title, state },
  onArchiveTask,
  onTogglePinTask,
  onEditTitle,
}) {
  return (
    <div
      className={`list-item ${state}`}
      role="listitem"
      aria-label={`task-${id}`}
    >
      <label
        htmlFor="checked"
        aria-label={`archiveTask-${id}`}
        className="checkbox"
      >
        <input
          type="checkbox"
          disabled={true}
          name="checked"
          id={`archiveTask-${id}`}
          checked={state === "TASK_ARCHIVED"}
        />
        <span
          className="checkbox-custom"
          onClick={() => onArchiveTask("ARCHIVE_TASK", id)}
          role="button"
          aria-label={`archiveButton-${id}`}
        />
      </label>

      <label htmlFor="title" aria-label={title} className="title">
        <input
          type="text"
          value={title}
          name="title"
          placeholder="Input title"
          style={{ textOverflow: "ellipsis" }}
          onChange={(e) => onEditTitle(e.target.value, id)}
        />
      </label>

      {state !== "TASK_ARCHIVED" && (
        <button
          className="pin-button"
          onClick={() => onTogglePinTask(state, id)}
          id={`pinTask-${id}`}
          aria-label={state === "TASK_PINNED" ? "unpin" : "pin"}
          key={`pinTask-${id}`}
        >
+         <span className={`icon-star`} />
        </button>
      )}
    </div>
  );
}

Task.propTypes = {
  /** Composition of the task */
  task: PropTypes.shape({
    /** Id of the task */
    id: PropTypes.string.isRequired,
    /** Title of the task */
    title: PropTypes.string.isRequired,
    /** Current state of the task */
    state: PropTypes.string.isRequired,
  }),
  /** Event to change the task to archived */
  onArchiveTask: PropTypes.func.isRequired,
  /** Event to change the task to pinned */
  onTogglePinTask: PropTypes.func.isRequired,
  /** Event to change the task title */
  onEditTitle: PropTypes.func.isRequired,
};

您現在將看到差異。

回歸測試確保我們不會意外引入變更。但仍然由您決定變更是否為故意的。

✅ 如果變更是故意的,請按一下「接受」。新的快照現在將設定為基準。

❌ 如果變更不是故意的,請按一下「拒絕」。建置將會失敗。修正程式碼並再次執行 Chromatic。

在我們的案例中,變更是故意的。繼續並按一下所有故事的「接受」。整個工作流程如下圖所示。

Build in storybook and run visual tests with Chromatic. If changes look good, then merge your PR.

阻止一個錯誤變成多個

一點點洩漏的 CSS 或一個損壞的元件可能會像滾雪球般變成多個問題。這些錯誤特別令人沮喪,難以偵錯。在下一章中,我們將以這些概念為基礎,學習如何捕捉這種級聯問題。

讓您的程式碼與本章保持同步。在 GitHub 上查看 6a337af。
這個免費指南對您有幫助嗎?發推文表示讚賞並幫助其他開發人員找到它。
下一章
組合
防止小變更演變成重大回歸錯誤
✍️ 在 GitHub 上編輯 – 歡迎提交 PR!
加入社群
6,721位開發人員及持續增加中
為何為何選擇 Storybook元件驅動的 UI
開源軟體
Storybook - Storybook 繁體中文

特別感謝 Netlify 以及 CircleCI