返回部落格

元件測試 RSCs

在瀏覽器中快速完整測試 React 伺服器組件

loading
Michael Shilman
@mshilman
上次更新

在眾所期待之後,React 伺服器組件 (RSCs) 正在轉變我們建構 React 應用程式的方式,模糊了傳統前端和後端程式碼之間的界線。RSCs 幫助您建構更快、更靈敏且更不複雜的應用程式。但儘管有如此巨大的推動力,關於如何測試它們的工作卻很少。這使得對它們的建構難以充滿信心。

在這篇文章中,我們介紹了 Storybook 元件測試對 RSCs 的支援。我們將展示:

  1. 現今 RSC 測試方面存在缺口。
  2. 您可以使用在瀏覽器中執行的 RSC 整合測試來彌合這個缺口。
  3. 您可以使用這些測試來執行完整的應用程式。在本例中,為 Vercel 的 Notes 示範應用程式。
  4. 您可以模擬複雜的應用程式狀態,例如身份驗證和直接資料庫存取。
  5. 這些測試比同等的端對端 (E2E) 測試快得多。

重要的是要事先聲明,我們並非建議擺脫 E2E 測試。E2E 是唯一能讓您對整個應用程式堆疊協同工作充滿信心的途徑。然而,常見的情況是有數百甚至數千個元件測試來執行許多關鍵應用程式狀態。根據我們的經驗,由於 E2E 測試速度較慢且不穩定性較高,這種程度的 E2E 測試是不可行的。

A spectrum titled When to test RSCs, going from In dev to In CI. Component tests in Storybook span the spectrum. End-to-end tests in Playwright only spans the end near In CI.

RSCs 和測試缺口

React 伺服器組件 (RSCs) 改變了 React 應用程式的編寫方式,引入了新的結構以換取效能和安全性方面的提升。現在還處於早期階段,但隨著 Next.js 團隊正在進行的工作以及 React 19 即將適用於其他框架,RSCs 注定會成為一個重要的議題。

現在是開始弄清楚最佳實務的時候了。

關於 RSCs 的最大問題之一是如何測試它們。 RSCs 在伺服器上執行,可以直接參考 Node 程式碼,但它們也包含僅在瀏覽器中呈現的用戶端元件。由於這種複雜性,React 核心團隊建議將端對端 (E2E) 測試作為測試 RSCs 的主要方式。E2E 測試會執行整個系統,並且不關心實作細節,因此它們是測試使用者實際體驗的絕佳方式。但是,它們也可能難以設定、速度慢且不穩定。

因此,嚴謹的測試人員會結合使用 E2E 測試來測試應用程式的順利路徑,以及單元/整合測試來涵蓋更多狀態。但在目前為止,RSCs 還沒有單元/整合方法,這在測試策略中留下了一個主要的缺口。

這就是 Storybook 的用武之地。在這篇文章中,我們介紹了 RSCs 的元件測試:在瀏覽器中執行的小型、獨立的整合測試,可同時執行伺服器和用戶端程式碼。您可以模擬各種測試案例,並快速且無不穩定性地執行它們。

A trophy labeled with different kinds of tests. Component tests make up the bulk of the trophy.
元件測試在您的測試策略中的位置

元件測試 RSCs

元件測試是小型、類似單元的測試,在瀏覽器中執行。與端對端 (E2E) 測試不同,後者可能涉及多次往返伺服器的行程,甚至超出此範圍的資料擷取,Storybook 元件測試是精簡、隔離的,並且完全在瀏覽器中執行。

考慮一個在隔離狀態下對單個 Button 元件進行操作的「Hello World」範例

// Button.stories.jsx
export const TestEvents = {
  async play({ mount, args }) {
    // Arrange
    let count = 0;
    const canvas = await mount(
      <Button label="Submit" onClick={() => { count++ }} />
    );

    // Act	  
    await userEvent.click(canvas.getByRole('button'));

    // Assert
    await expect(canvas.getByText('Submit')).toBeDefined();
    await expect(count).toBe(1);
  }
}

對於任何編寫過 story 的 play 函數的人來說,這個範例看起來會很熟悉,它允許您模擬和斷言元件的功能和行為。這裡唯一的新建構是 mount 函數,它是 Storybook 最近新增的功能,允許您在 play 函數中「安排、執行、斷言」所有操作

  1. 安排: (a) 初始化 count,(b) mount(...),它會呈現 story。
  2. 執行: 按一下按鈕
  3. 斷言: 在按一下按鈕後,驗證 (a) 呈現了「Submit」按鈕,以及 (b) 計數已更新。

請注意,如果您不解構 mount 函數,play 將在 Storybook 自動呈現 Story 後執行。

這不是最有趣的測試,但正如我們接下來將看到的,建構和測試完整的 RSC 應用程式頁面並不需要比這多多少。

伺服器元件 Notes 示範應用程式

元件測試不僅僅適用於 Hello World。過去,我們使用 Storybook 在 RSC 中建構了一個 Hacker News 克隆版本。該範例是在 REST API 的基礎上實作的,我們使用 Mock Service Worker (MSW) 模擬了這些 API 呼叫。

但並非所有 RSC 應用程式都那麼簡單。RSC 幾乎可以在伺服器上執行任何操作,您可能需要其他種類的模擬才能在瀏覽器中測試該功能。這就是為什麼,例如,我們投資了 Storybook 中的型別安全模組模擬

這次,我們修改了 Vercel 的 伺服器元件 Notes 示範應用程式,以隔離方式建構和測試 RSCs。Notes 應用程式使用 Prisma 資料庫來儲存筆記,並使用 Github OAuth 進行身份驗證。

當使用者登出時,他們可以檢視和搜尋筆記

View and searching notes while logged out

登入後,他們也可以新增/編輯/刪除筆記

Adding, editing, then deleting a note

我們將 Storybook 組織成兩個部分:「App」和「Components」。App 反映了應用程式的 route/folder 結構,而 Components 目前是應用程式內部元件的平面列表。對於更完整的應用程式,這些元件將被組織到子資料夾中

Storybook with expanded subfolders in the sidebar

使用記憶體內資料庫進行模擬

資料庫是許多 Web 應用程式的核心,Notes 示範也不例外。我們使用 Prisma 資料庫來儲存筆記。使用記憶體內資料庫對其進行模擬,使我們能夠控制螢幕上呈現的筆記,並攔截新增、移除和更新筆記的時間。

這非常驚人,原因有很多:

  1. 速度。 我們可以立即將我們的應用程式傳送到任何狀態並從那裡開始測試,從而消除移動部件。更少的網路請求和移動部件意味著這些測試比其 E2E 等效測試快得多,並且不穩定性也少得多。
  2. 覆蓋率。 我們在單一組整合測試中獲得了前端和後端程式碼的測試覆蓋率,因為我們是在後端的最後端進行模擬。這比必須費盡周折來衡量 E2E 執行期間前端和後端的覆蓋率,然後在事後將這些報告拼接在一起要容易得多。
  3. 隔離。 由於資料庫在記憶體中,這意味著每個測試實際上都有自己的資料庫,您永遠不必擔心不同的測試會覆寫彼此的資料。如果您有針對固定資料庫執行的 E2E 測試,則在單次執行中平行執行測試或執行多次執行時,您始終需要擔心資源爭用。在這裡永遠不會有問題。

我們稍後將量化這些優點,但首先讓我們看看它是如何運作的。以下是實作 Notes 頁面的伺服器元件:

// apps/notes/[id]/page.tsx
import NoteUI from '#components/note-ui';
import { db } from '#lib/db';

type Props = { params: { id: string } };

export default async function Page({ params }: Props) {
  const note = await db.note.findUnique({ where: { id: Number(params.id) } });
  if (note === null) { /* error */ }
  return <NoteUI note={note} isEditing={false} />;
}

所有魔法都發生在這行程式碼上:

import { db } from '#lib/db';

我們正在使用一個稱為子路徑匯入的標準來啟用 型別安全模組模擬。在應用程式環境 (Next.js) 中執行時,這會從 /lib/db.ts 匯入,後者匯出連線到真實資料庫的 Prisma 用戶端。在 Storybook 中執行時,它會從 /lib/db.mock.ts 匯入,後者匯出連線到記憶體內資料庫的 Prisma 用戶端。

接下來,讓我們看一下其中一個 story:

// app/notes/[id]/page.stories.jsx
import type { Meta, StoryObj } from '@storybook/react';
import { db, initializeDB } from '#lib/db.mock';
import Page from './page';
import { PageDecorator } from '#.storybook/decorators';

export default {
  component: Page,
  async beforeEach() {
    await db.note.create({
      data: {
        title: 'Module mocking in Storybook?',
        body: "Yup, that's a thing now! 🎉",
        createdBy: 'storybookjs',
      },
    });
    await db.note.create({ /* another */ });
  },
  decorators: [PageDecorator],
  parameters: {
    layout: 'fullscreen',
    nextjs: {
      navigation: { pathname: '/note/1' },
    },
  },
  args: { params: { id: '1' } },
};

export const NotLoggedIn = {}

export const EmptyState = {
  async play({ mount }) {
    initializeDB({});
    await mount();
  },
}

透過這個小程式碼片段,檔案中的每個 story 預設都會有兩個筆記。特定的 story 可以修改資料庫內容以達到其所需的狀態,例如 EmptyState 會重設為空的資料庫。

請注意,與 Page 元件不同,story 檔案直接從 '#lib/db.mock' 匯入。這意味著它為模擬提供了完整的型別安全性,例如,包裝的函數會公開其 .mock.calls 和其他欄位,以進行型別檢查和自動完成。

模擬身份驗證

現在讓我們看一下身份驗證。與需要複雜的歌舞表演才能驗證使用者身份的 E2E 測試不同,在 Storybook 中模擬經過身份驗證的狀態非常簡單。以下是另一個 story,顯示與上述相同的頁面,但處於登入狀態:

// app/notes/[id]/page.stories.jsx

// ...Continuing from above

import { cookies } from '@storybook/nextjs/headers.mock';
import { createUserCookie, userCookieKey } from '#lib/session';

// export default { ... } from above

export const LoggedIn = {
  async beforeEach() {
    cookies().set(userCookieKey, await createUserCookie('storybookjs'));
  }
}

由於我們的身份驗證是基於 Cookie 的,並且 Storybook 的 Next.js 框架會自動模擬 Cookie,因此我們只需設定身份驗證 Cookie 即可完成。透過這個小的修改,RSC 將看起來並表現得如同使用者 storybookjs 已登入一樣。

測試使用者工作流程

模擬資料庫和模擬身份驗證這兩個簡單的原語為我們帶來了很多好處。我們可以編寫速度極快且無不穩定性的 E2E 樣式測試,並且還允許我們檢查系統的任何部分。例如,以下是新增新筆記的測試:

// app/note/edit/page.stories.jsx

// ...Continuing from above

export const SaveNewNote = {
  play: async ({ mount }) => {
	  // Arrange
    cookies().set(userCookieKey, await createUserCookie('storybookjs'));
    const canvas = await mount();
    
    // Act
    const titleInput = await canvas.findByLabelText(
      'Enter a title for your note',
    )
    const bodyInput = await canvas.findByLabelText(
      'Enter the body for your note',
    )
    await userEvent.clear(titleInput)
    await userEvent.type(titleInput, 'New Note Title')
    await userEvent.type(bodyInput, 'New Note Body')
    await userEvent.click(
      await canvas.findByRole('menuitem', { name: /done/i }),
    )
    
    // Assert
    await waitFor(() =>
      expect(getRouter().push).toHaveBeenLastCalledWith('/note/1', expect.anything()),
    );
    await expect(await db.note.findUnique({ where: { id: 1 } })).toEqual(
      expect.objectContaining({
        title: 'New Note Title',
        body: 'New Note Body',
      }),
    )
  },
}

整合在一起

在這篇文章的開頭,我們聲稱元件測試可以是測試 RSC 應用程式的快速且無不穩定性的方法。那麼我們做得如何呢?

我們的 Storybook 透過上述 story 等方式展示了各種型別的模擬,並且在 storybook-rsc-demo 儲存庫中公開提供。截至撰寫本文時,它包含 34 個 story。

在配備 16GB RAM 的 2021 Macbook M1 Pro 上,將這些 story 作為 Vitest 測試(透過 Storybook Test)執行大約需要 7 秒。它在整個專案(包括前端和後端程式碼)中產生了 87% 的行覆蓋率和 73% 的分支覆蓋率。

Storybook Test 可以在 CLI 或 Storybook 本身中執行您的測試,後者提供狀態篩選和互動式偵錯器,可讓您逐步回溯和前進測試步驟。而且由於您的測試是 story,因此您在編寫測試時也會獲得視覺回饋。

0:00
/0:13
👋
搶先體驗 Storybook Test。我們的搶先體驗計畫包括來自 Storybook 維護者的實務協助、獨家活動以及使用者社群的存取權,以協助為您的專案建構 UI 測試套件。
👉 在此註冊!

立即試用

RSC 的元件測試在 Storybook 中(實驗性地)可用。在新 Next.js 專案中試用

npx storybook@latest init

或升級現有專案

npx storybook@latest upgrade

確保您啟用 RSC 支援的功能旗標

// .storybook/main.js
export default {
  // ...
  features: {
    experimentalRSC: true,
  },
};

如需本文中顯示的完整範例,請參閱 storybook-rsc-demo 儲存庫。若要瞭解更多資訊,請參閱 RSC元件測試 文件。

下一步是什麼?

本文中基準測試的 Storybook Test 在 Storybook 8.4 中可用。此外,我們也在努力:

  1. 零配置程式碼覆蓋率,以量化您在開發時的測試
  2. 重點測試,以快速測試單個 story、元件或目錄
  3. 模擬功能,以形式化和視覺化我們在此處展示的一些模式

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

加入 Storybook 電子郵件列表

獲取最新消息、更新和發行資訊

6,730位開發人員以及更多

我們正在招聘!

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

查看職缺

熱門文章

Storybook 標籤

組織您的元件和 story,以符合您的工作方式
loading
Michael Shilman

Storybook 8.5

觸手可及的易用性
loading
Michael Shilman

Storybook 8.4

一鍵在瀏覽器中進行元件測試
loading
Michael Shilman
加入社群
6,730位開發人員以及更多
為何為何選擇 Storybook元件驅動的 UI
文件指南教學課程變更日誌遙測
社群外掛參與其中部落格
展示探索專案元件詞彙表
開放原始碼軟體
Storybook - Storybook 繁體中文

特別感謝 Netlify CircleCI