目錄
開始進行React 測試
選項1:單元測試
選項2:集成測試
那麼,什麼需要單元測試?
其他好處
清晰的waitFor 塊
行內it 註釋
團隊的後續步驟
首頁 web前端 css教學 反應集成測試:覆蓋範圍更大,測試較少

反應集成測試:覆蓋範圍更大,測試較少

Apr 07, 2025 am 09:20 AM

React Integration Testing: Greater Coverage, Fewer Tests

對於像使用React 構建的交互式網站,集成測試是自然而然的選擇。它們驗證用戶與應用程序的交互方式,而無需端到端測試的額外開銷。

本文通過一個練習來闡述,該練習從一個簡單的網站開始,使用單元測試和集成測試驗證行為,並演示集成測試如何通過更少的代碼行實現更大的價值。本文內容假設您熟悉React 和JavaScript 中的測試。熟悉Jest 和React Testing Library 會有所幫助,但不是必需的。

測試分為三種類型:

  • 單元測試獨立驗證一段代碼。它們易於編寫,但可能會忽略大局。
  • 端到端測試(E2E) 使用自動化框架(例如Cypress 或Selenium)像用戶一樣與您的網站交互:加載頁面、填寫表單、點擊按鈕等。它們通常編寫和運行速度較慢,但與真實的用戶體驗非常接近。
  • 集成測試介於兩者之間。它們驗證應用程序的多個單元如何協同工作,但比E2E 測試更輕量級。例如,Jest 自帶一些內置實用程序來促進集成測試;Jest 在後台使用jsdom 來模擬常見的瀏覽器API,其開銷小於自動化,並且其強大的模擬工具可以模擬外部API 調用。

另一個需要注意的地方:在React 應用程序中,單元測試和集成測試的編寫方式相同,使用的工具也相同

開始進行React 測試

我創建了一個簡單的React 應用程序(可在GitHub 上找到),其中包含一個登錄表單。我將其連接到reqres.in,這是一個我發現用於測試前端項目的方便的API。

您可以成功登錄:

…或者遇到來自API 的錯誤消息:

代碼結構如下:

 <code>LoginModule/ ├── components/ │ ├── Login.js // 渲染LoginForm、错误消息和登录确认│ └── LoginForm.js // 渲染登录表单字段和按钮├── hooks/ │ └── useLogin.js // 连接到API 并管理状态└── index.js // 将所有内容整合在一起</code>
登入後複製

選項1:單元測試

如果您像我一樣喜歡編寫測試——也許戴著耳機,在Spotify 上播放著不錯的音樂——那麼您可能會忍不住為每個文件編寫單元測試。

即使您不是測試愛好者,您也可能正在參與一個“試圖做好測試”的項目,但沒有明確的策略,測試方法是“我想每個文件都應該有自己的測試?”

這看起來像這樣(為了清晰起見,我在測試文件名中添加了unit):

 <code>LoginModule/ ├── components/ │  ├── Login.js │  ├── Login.unit.test.js │  ├── LoginForm.js │  └── LoginForm.unit.test.js ├── hooks/ │  ├── useLogin.js │  └── useLogin.unit.test.js ├── index.js └── index.unit.test.js</code>
登入後複製

我在GitHub 上完成了添加所有這些單元測試的練習,並創建了一個test:coverage:unit 腳本以生成覆蓋率報告(Jest 的內置功能)。我們可以通過四個單元測試文件實現100% 的覆蓋率:

100% 的覆蓋率通常是過度的,但對於如此簡單的代碼庫來說是可以實現的。

讓我們深入研究為onLogin React hook 創建的單元測試之一。如果您不熟悉React hook 或如何測試它們,請不要擔心。

 test('successful login flow', async () => {
 // 模擬成功的API 響應 jest
  .spyOn(window, 'fetch')
  .mockResolvedValue({ json: () => ({ token: '123' }) });

 const { result, waitForNextUpdate } = renderHook(() => useLogin());

 act(() => {
  result.current.onSubmit({
   email: '[email protected]',
   password: 'password',
  });
 });

 // 將狀態設置為pending
 expect(result.current.state).toEqual({
  status: 'pending',
  user: null,
  error: null,
 });

 await waitForNextUpdate();

 // 將狀態設置為resolved,存儲電子郵件地址 expect(result.current.state).toEqual({
  status: 'resolved',
  user: {
   email: '[email protected]',
  },
  error: null,
 });
});
登入後複製

這個測試寫起來很有趣(因為React Hooks Testing Library 使測試hook 變得輕而易舉),但它有一些問題。

首先,測試驗證內部狀態從'pending' 更改為'resolved';此實現細節不會向用戶公開,因此,可能不是要測試的好東西。如果我們重構應用程序,我們將不得不更新此測試,即使從用戶的角度來看沒有任何變化。

此外,作為單元測試,這只是其中一部分。如果我們想驗證登錄流程的其他功能,例如提交按鈕文本更改為“加載中”,我們將不得不在不同的測試文件中進行操作。

選項2:集成測試

讓我們考慮添加一個集成測試來驗證此流程的替代方法:

 <code>LoginModule/ ├── components/ │  ├── Login.js │  └── LoginForm.js ├── hooks/ │  └── useLogin.js ├── index.js └── index.integration.test.js</code>
登入後複製

我實現了這個測試和一個test:coverage:integration 腳本以生成覆蓋率報告。就像單元測試一樣,我們可以達到100% 的覆蓋率,但這次都在一個文件中,並且需要的代碼行更少。

以下是涵蓋成功登錄流程的集成測試:

 test('successful login', async () => {
  jest
    .spyOn(window, 'fetch')
    .mockResolvedValue({ json: () => ({ token: '123' }) });

  render(<loginmodule></loginmodule> );

  const emailField = screen.getByRole('textbox', { name: 'Email' });
  const passwordField = screen.getByLabelText('Password');
  const button = screen.getByRole('button');

  // 填寫並提交表單fireEvent.change(emailField, { target: { value: '[email protected]' } });
  fireEvent.change(passwordField, { target: { value: 'password' } });
  fireEvent.click(button);

  // 它設置加載狀態expect(button).toBeDisabled();
  expect(button).toHaveTextContent('Loading...');

  await waitFor(() => {
    // 它隱藏表單元素expect(button).not.toBeInTheDocument();
    expect(emailField).not.toBeInTheDocument();
    expect(passwordField).not.toBeInTheDocument();

    // 它顯示成功文本和電子郵件地址const loggedInText = screen.getByText('Logged in as');
    expect(loggedInText).toBeInTheDocument();
    const emailAddressText = screen.getByText('[email protected]');
    expect(emailAddressText).toBeInTheDocument();
  });
});
登入後複製

我真的很喜歡這個測試,因為它從用戶的角度驗證了整個登錄流程:表單、加載狀態和成功確認消息。集成測試非常適合React 應用程序,正是因為這種用例;用戶體驗是我們想要測試的內容,而這幾乎總是涉及多個不同的代碼片段協同工作

此測試不了解使預期行為起作用的組件或hook,這很好。只要用戶體驗保持不變,我們就可以重寫和重構這些實現細節而不會破壞測試。

我不會深入研究登錄流程的初始狀態和錯誤處理的其他集成測試,但我鼓勵您在GitHub 上查看它們。

那麼,什麼需要單元測試?

與其考慮單元測試與集成測試,不如讓我們退一步,考慮一下我們如何決定首先需要測試什麼。需要測試LoginModule,因為它是一個我們希望使用者(應用程序中的其他文件)能夠放心地使用的實體。

另一方面,不需要測試onLogin hook,因為它只是LoginModule 的實現細節。但是,如果我們的需求發生變化,並且onLogin 在其他地方有用例,那麼我們將需要添加我們自己的(單元)測試來驗證其作為可重用實用程序的功能。 (我們也需要移動該文件,因為它不再特定於LoginModule 了。)

單元測試仍然有很多用例,例如需要驗證可重用選擇器、hook 和普通函數。在開發代碼時,您可能還會發現使用單元測試進行測試驅動開發很有幫助,即使您稍後將該邏輯向上移動到集成測試。

此外,單元測試在針對多個輸入和用例進行詳盡測試方面做得很好。例如,如果我的表單需要針對各種場景(例如無效電子郵件、缺少密碼、密碼過短)顯示內聯驗證,我將在集成測試中涵蓋一個代表性案例,然後在單元測試中深入研究具體案例。

其他好處

既然我們在這裡,我想談談一些幫助我的集成測試保持清晰和有序的語法技巧。

清晰的waitFor 塊

我們的測試需要考慮LoginModule 的加載狀態和成功狀態之間的延遲:

 const button = screen.getByRole('button');
fireEvent.click(button);

expect(button).not.toBeInTheDocument(); // 太快了,按鈕還在!
登入後複製

我們可以使用DOM Testing Library 的waitFor 輔助函數來做到這一點:

 const button = screen.getByRole('button');
fireEvent.click(button);

await waitFor(() => {
 expect(button).not.toBeInTheDocument(); // 啊,好多了});
登入後複製

但是,如果我們還想測試其他一些項目呢?網上沒有很多關於如何處理此問題的好的示例,並且在過去的項目中,我已經將其他項目放在waitFor 之外:

 // 等待按鈕await waitFor(() => {
 expect(button).not.toBeInTheDocument();
});

// 然後測試確認消息const confirmationText = getByText('Logged in as [email protected]');
expect(confirmationText).toBeInTheDocument();
登入後複製

這有效,但我不喜歡它,因為它使按鈕條件看起來很特殊,即使我們可以輕鬆地切換這些語句的順序:

 // 等待確認消息await waitFor(() => {
 const confirmationText = getByText('Logged in as [email protected]');
 expect(confirmationText).toBeInTheDocument();
});

// 然後測試按鈕expect(button).not.toBeInTheDocument();
登入後複製

在我看來,將與相同更新相關的所有內容一起分組到waitFor 回調中要好得多:

 await waitFor(() => {
 expect(button).not.toBeInTheDocument();

 const confirmationText = screen.getByText('Logged in as [email protected]');
 expect(confirmationText).toBeInTheDocument();
});
登入後複製

對於像這樣的簡單斷言,我真的很喜歡這種技術,但在某些情況下,它可能會減慢測試速度,等待在waitFor 之外立即發生的失敗。有關此示例,請參閱React Testing Library 常用錯誤中的“在單個waitFor 回調中有多個斷言”。

對於包含幾個步驟的測試,我們可以連續使用多個waitFor 塊:

 const button = screen.getByRole('button');
const emailField = screen.getByRole('textbox', { name: 'Email' });

// 填寫表單fireEvent.change(emailField, { target: { value: '[email protected]' } });

await waitFor(() => {
 // 檢查按鈕是否已啟用 expect(button).not.toBeDisabled();
  expect(button).toHaveTextContent('Submit');
});

// 提交表單fireEvent.click(button);

await waitFor(() => {
 // 檢查按鈕是否不再存在 expect(button).not.toBeInTheDocument();
});
登入後複製

如果您只等待一個項目出現,則可以使用findBy 查詢代替。它在後台使用waitFor。

行內it 註釋

另一個測試最佳實踐是編寫更少、更長的測試;這使您可以將測試用例與重要的用戶流程關聯起來,同時使測試保持隔離,以避免意外行為。我贊成這種方法,但它在保持代碼組織和記錄所需行為方面可能會帶來挑戰。我們需要未來的開發人員能夠返回測試並了解它在做什麼,為什麼它會失敗等等。

例如,假設這些期望之一開始失敗:

 it('handles a successful login flow', async () => {
 // 為清晰起見隱藏測試的開頭

  expect(button).toBeDisabled();
  expect(button).toHaveTextContent('Loading...');


 await waitFor(() => {
  expect(button).not.toBeInTheDocument();
  expect(emailField).not.toBeInTheDocument();
  expect(passwordField).not.toBeInTheDocument();


  const confirmationText = screen.getByText('Logged in as [email protected]');
  expect(confirmationText).toBeInTheDocument();
 });
});
登入後複製

查看此內容的開發人員無法輕鬆確定正在測試的內容,並且可能難以確定失敗是錯誤(這意味著我們應該修復代碼)還是行為更改(這意味著我們應該修復測試)。

我最喜歡的解決方案是使用每個測試的鮮為人知的測試語法,並添加描述正在測試的每個關鍵行為的行內it 樣式註釋:

 test('successful login', async () => {
 // 為清晰起見隱藏測試的開頭

 // 它設置加載狀態 expect(button).toBeDisabled();
  expect(button).toHaveTextContent('Loading...');


 await waitFor(() => {
  // 它隱藏表單元素  expect(button).not.toBeInTheDocument();
  expect(emailField).not.toBeInTheDocument();
  expect(passwordField).not.toBeInTheDocument();


  // 它顯示成功文本和電子郵件地址  const confirmationText = screen.getByText('Logged in as [email protected]');
  expect(confirmationText).toBeInTheDocument();
 });
});
登入後複製

這些註釋不會神奇地與Jest 集成,因此如果您遇到失敗,失敗的測試名稱將對應於您傳遞給測試標籤的參數,在本例中為“successful login”。但是,Jest 的錯誤消息包含周圍的代碼,因此這些it 註釋仍然有助於識別失敗的行為。當我從一個期望中刪除not 時,我收到了以下錯誤消息:

為了獲得更明確的錯誤,有一個名為jest-expect-message 的包允許您為每個期望定義錯誤消息:

 expect(button, 'button is still in document').not.toBeInTheDocument();
登入後複製

一些開發人員更喜歡這種方法,但我發現它在大多數情況下有點granular 了,因為單個it 通常涉及多個期望。

團隊的後續步驟

有時我希望我們可以為人類製定linter 規則。如果是這樣,我們可以為我們的團隊設置一個prefer-integration-tests 規則,然後就結束了。

但是,唉,我們需要找到一個更類似的解決方案來鼓勵開發人員在某些情況下選擇集成測試,例如我們前面介紹的LoginModule 示例。像大多數事情一樣,這歸結於團隊討論您的測試策略,就對項目有意義的內容達成一致,並且——希望——在ADR 中記錄它。

在製定測試計劃時,我們應該避免一種會迫使開發人員為每個文件編寫測試的文化。開發人員需要能夠放心地做出明智的測試決策,而不必擔心他們“測試不足”。 Jest 的覆蓋率報告可以通過提供一個健全性檢查來幫助解決這個問題,即使測試在集成級別上進行了合併。

我仍然不認為自己是集成測試專家,但是進行這項練習幫助我分解了一個集成測試比單元測試提供更大價值的用例。我希望與您的團隊分享這一點,或者在您的代碼庫上進行類似的練習,將有助於指導您將集成測試納入您的工作流程。

以上是反應集成測試:覆蓋範圍更大,測試較少的詳細內容。更多資訊請關注PHP中文網其他相關文章!

本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

Video Face Swap

Video Face Swap

使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱工具

記事本++7.3.1

記事本++7.3.1

好用且免費的程式碼編輯器

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用

禪工作室 13.0.1

禪工作室 13.0.1

強大的PHP整合開發環境

Dreamweaver CS6

Dreamweaver CS6

視覺化網頁開發工具

SublimeText3 Mac版

SublimeText3 Mac版

神級程式碼編輯軟體(SublimeText3)

熱門話題

Java教學
1664
14
CakePHP 教程
1423
52
Laravel 教程
1317
25
PHP教程
1268
29
C# 教程
1244
24
如何使用HTML,CSS和JavaScript創建動畫倒計時計時器 如何使用HTML,CSS和JavaScript創建動畫倒計時計時器 Apr 11, 2025 am 11:29 AM

您是否曾經在項目上需要一個倒計時計時器?對於這樣的東西,可以自然訪問插件,但實際上更多

HTML數據屬性指南 HTML數據屬性指南 Apr 11, 2025 am 11:50 AM

您想了解的有關HTML,CSS和JavaScript中數據屬性的所有信息。

使Sass更快的概念證明 使Sass更快的概念證明 Apr 16, 2025 am 10:38 AM

在一個新項目開始時,Sass彙編發生在眼睛的眨眼中。感覺很棒,尤其是當它與browsersync配對時,它重新加載

當您看上去時,CSS梯度變得更好 當您看上去時,CSS梯度變得更好 Apr 11, 2025 am 09:16 AM

我關注的一件事是Lea Verou&#039; s conic-Gradient()Polyfill的功能列表是最後一項:

靜態表單提供商的比較 靜態表單提供商的比較 Apr 16, 2025 am 11:20 AM

讓我們嘗試在這裡造成一個術語:“靜態表單提供商”。你帶上html

如何在WordPress主題中構建VUE組件 如何在WordPress主題中構建VUE組件 Apr 11, 2025 am 11:03 AM

內聯式模板指令使我們能夠將豐富的VUE組件構建為對現有WordPress標記的逐步增強。

php是A-OK用於模板 php是A-OK用於模板 Apr 11, 2025 am 11:04 AM

PHP模板通常會因促進Subpar代碼而變得不良說唱,但這並不是這樣的情況。讓我們看一下PHP項目如何執行基本的

三種代碼 三種代碼 Apr 11, 2025 pm 12:02 PM

每次啟動一個新項目時,我都會將我正在查看的代碼分為三種類型,或者如果您願意的話。我認為這些類型可以應用於

See all articles