通過 React 集成測試提高代碼可維護性
已發表: 2022-03-11集成測試是測試成本和價值之間的最佳平衡點。 在 react-testing-library 的幫助下為 React 應用程序編寫集成測試,而不是組件單元測試,可以在不影響開發速度的情況下提高代碼的可維護性。
如果您想在我們繼續之前搶先一步,您可以在此處查看如何使用 react-testing-library 進行 React 應用程序集成測試的示例。
為什麼投資集成測試?
“集成測試在信心和速度/費用之間的權衡上取得了很好的平衡。這就是為什麼建議將大部分精力(不是全部,請注意)花在上面的原因。”
– Kent C. Dodds 在寫測試中。 不是很多。 主要是集成。
為 React 組件編寫單元測試是一種常見的做法,通常使用流行的庫來測試 React “酶”; 具體來說,它的“淺”方法。 這種方法使我們能夠獨立於應用程序的其餘部分來測試組件。 然而,由於編寫 React 應用程序就是組合組件,單靠單元測試並不能確保應用程序沒有錯誤。
例如,更改組件接受的 props 並更新其關聯的單元測試可能會導致所有測試通過,而如果另一個組件未相應更新,則應用程序仍可能會損壞。
集成測試有助於在對 React 應用程序進行更改時保持安心,因為它們確保組件的組合產生所需的用戶體驗。
React 應用集成測試的要求
以下是 React 開發人員在編寫集成測試時想要做的一些事情:
- 從用戶的角度測試應用程序用例。 用戶訪問網頁上的信息並與可用控件進行交互。
- 模擬 API 調用不依賴於 API 可用性和狀態來通過/失敗測試。
- 模擬瀏覽器 API(例如,本地存儲),因為它們根本不存在於測試環境中。
- 斷言 React DOM 狀態(瀏覽器 DOM 或原生移動環境)。
現在,在編寫 React 應用程序集成測試時,我們應該盡量避免一些事情:
- 測試實施細節。 實現更改只有在確實引入了錯誤時才應該中斷測試。
- 嘲諷太多。 我們想測試應用程序的所有部分如何協同工作。
- 淺渲染。 我們想要測試應用程序中所有組件的組成,直到最小的組件。
為什麼選擇 React-testing-library?
上述要求使 react-testing-library 成為一個不錯的選擇,因為它的主要指導原則是允許以類似於實際人類使用方式的方式測試 React 組件。
該庫及其可選的配套庫允許我們編寫與 DOM 交互並斷言其狀態的測試。
示例應用程序設置
我們將為其編寫示例集成測試的應用程序實現了一個簡單的場景:
- 用戶輸入 GitHub 用戶名。
- 該應用程序顯示與輸入的用戶名關聯的公共存儲庫列表。
從集成測試的角度來看,如何實現上述功能應該是無關緊要的。 然而,為了接近真實世界的應用程序,該應用程序遵循常見的 React 模式,因此該應用程序:
- 是一個單頁應用程序 (SPA)。
- 發出 API 請求。
- 具有全局狀態管理。
- 支持國際化。
- 使用 React 組件庫。
應用程序實現的源代碼可以在這裡找到。
編寫集成測試
安裝依賴項
用紗線:
yarn add --dev jest @testing-library/react @testing-library/user-event jest-dom nock
或者使用 npm:
npm i -D jest @testing-library/react @testing-library/user-event jest-dom nock
創建集成測試套件文件
我們將在應用程序的./test
文件夾中創建一個名為viewGitHubRepositoriesByUsername.spec.js
的文件。 Jest 會自動拾取它。
在測試文件中導入依賴
import React from 'react'; // so that we can use JSX syntax import { render, cleanup, waitForElement } from '@testing-library/react'; // testing helpers import userEvent from '@testing-library/user-event' // testing helpers for imitating user events import 'jest-dom/extend-expect'; // to extend Jest's expect with DOM assertions import nock from 'nock'; // to mock github API import { FAKE_USERNAME_WITH_REPOS, FAKE_USERNAME_WITHOUT_REPOS, FAKE_BAD_USERNAME, REPOS_LIST } from './fixtures/github'; // test data to use in a mock API import './helpers/initTestLocalization'; // to configure i18n for tests import App from '../App'; // the app that we are going to test
設置測試套件
describe('view GitHub repositories by username', () => { beforeAll(() => { nock('https://api.github.com') .persist() .get(`/users/${FAKE_USERNAME_WITH_REPOS}/repos`) .query(true) .reply(200, REPOS_LIST); }); afterEach(cleanup); describe('when GitHub user has public repositories', () => { it('user can view the list of public repositories for entered GitHub username', async () => { // arrange // act // assert }); }); describe('when GitHub user has no public repositories', () => { it('user is presented with a message that there are no public repositories for entered GitHub username', async () => { // arrange // act // assert }); }); describe('when GitHub user does not exist', () => { it('user is presented with an error message', async () => { // arrange // act // assert }); }); });
筆記:
- 在所有測試之前,模擬 GitHub API 以在使用特定用戶名調用時返回存儲庫列表。
- 每次測試後,清理測試 React DOM,以便每個測試從一個乾淨的位置開始。
-
describe
塊指定集成測試用例和流程變化。 - 我們正在測試的流量變化是:
- 用戶輸入具有關聯公共 GitHub 存儲庫的有效用戶名。
- 用戶輸入沒有關聯公共 GitHub 存儲庫的有效用戶名。
- 用戶輸入 GitHub 上不存在的用戶名。
-
it
阻止使用異步回調,因為他們正在測試的用例中有異步步驟。
編寫第一個流測試
首先,需要渲染應用程序。
const { getByText, getByPlaceholderText, queryByText } = render(<App />);
從@testing-library/react
模塊導入的render
方法在測試 React DOM 中渲染應用程序,並返回綁定到渲染的應用程序容器的 DOM 查詢。 這些查詢用於定位要與之交互和斷言的 DOM 元素。
現在,作為被測流程的第一步,用戶將看到一個用戶名字段並在其中鍵入一個用戶名字符串。
userEvent.type(getByPlaceholderText('userSelection.usernamePlaceholder'), FAKE_USERNAME_WITH_REPOS);
來自導入@testing-library/user-event
模塊的userEvent
助手有一個type
方法,可以模仿用戶在文本字段中鍵入文本時的行為。 它接受兩個參數:接受輸入的 DOM 元素和用戶鍵入的字符串。
用戶通常通過與它們關聯的文本來查找 DOM 元素。 在輸入的情況下,它可以是標籤文本或占位符文本。 之前從render
返回的getByPlaceholderText
查詢方法允許我們通過佔位符文本找到 DOM 元素。
請注意,由於文本本身經常會發生變化,因此最好不要依賴實際的本地化值,而是將本地化模塊配置為返回本地化項目鍵作為其值。
例如,當“en-US”本地化通常會返回Enter GitHub username
作為userSelection.usernamePlaceholder
鍵的值時,在測試中,我們希望它返回userSelection.usernamePlaceholder
。

當用戶在字段中鍵入文本時,他們應該會看到文本字段值已更新。
expect(getByPlaceholderText('userSelection.usernamePlaceholder')).toHaveAttribute('value', FAKE_USERNAME_WITH_REPOS);
接下來在流程中,用戶單擊提交按鈕並希望看到存儲庫列表。
userEvent.click(getByText('userSelection.submitButtonText').closest('button')); getByText('repositories.header');
userEvent.click
方法模擬用戶單擊 DOM 元素,而getByText
查詢通過其包含的文本查找 DOM 元素。 closest
的修飾符確保我們選擇正確種類的元素。
注意:在集成測試中,步驟通常同時服務於act
和assert
角色。 例如,我們斷言用戶可以通過點擊一個按鈕來點擊它。
在上一步中,我們斷言用戶看到了應用程序的存儲庫列表部分。 現在,我們需要斷言,由於從 GitHub 獲取存儲庫列表可能需要一些時間,因此用戶會看到正在獲取的指示。 我們還希望確保應用程序在仍在獲取存儲庫列表時不會告訴用戶沒有與輸入的用戶名關聯的存儲庫。
getByText('repositories.loadingText'); expect(queryByText('repositories.empty')).toBeNull();
請注意, getBy
查詢前綴用於斷言可以找到 DOM 元素,而queryBy
查詢前綴用於相反的斷言。 此外,如果未找到任何元素,則queryBy
不會返回錯誤。
接下來,我們要確保最終應用程序完成獲取存儲庫並將它們顯示給用戶。
await waitForElement(() => REPOS_LIST.reduce((elementsToWaitFor, repository) => { elementsToWaitFor.push(getByText(repository.name)); elementsToWaitFor.push(getByText(repository.description)); return elementsToWaitFor; }, []));
waitForElement
異步方法用於等待 DOM 更新,該更新將呈現作為方法參數 true 提供的斷言。 在這種情況下,我們斷言應用程序會顯示模擬的 GitHub API 返回的每個存儲庫的名稱和描述。
最後,應用程序不應再顯示正在獲取存儲庫的指示符,並且不應顯示錯誤消息。
expect(queryByText('repositories.loadingText')).toBeNull(); expect(queryByText('repositories.error')).toBeNull();
我們生成的 React 集成測試如下所示:
it('user can view the list of public repositories for entered GitHub username', async () => { const { getByText, getByPlaceholderText, queryByText } = render(<App />); userEvent.type(getByPlaceholderText('userSelection.usernamePlaceholder'), FAKE_USERNAME_WITH_REPOS); expect(getByPlaceholderText('userSelection.usernamePlaceholder')).toHaveAttribute('value', FAKE_USERNAME_WITH_REPOS); userEvent.click(getByText('userSelection.submitButtonText').closest('button')); getByText('repositories.header'); getByText('repositories.loadingText'); expect(queryByText('repositories.empty')).toBeNull(); await waitForElement(() => REPOS_LIST.reduce((elementsToWaitFor, repository) => { elementsToWaitFor.push(getByText(repository.name)); elementsToWaitFor.push(getByText(repository.description)); return elementsToWaitFor; }, [])); expect(queryByText('repositories.loadingText')).toBeNull(); expect(queryByText('repositories.error')).toBeNull(); });
備用流量測試
當用戶輸入沒有關聯公共存儲庫的 GitHub 用戶名時,應用程序會顯示相應的消息。
describe('when GitHub user has no public repositories', () => { it('user is presented with a message that there are no public repositories for entered GitHub username', async () => { const { getByText, getByPlaceholderText, queryByText } = render(<App />); userEvent.type(getByPlaceholderText('userSelection.usernamePlaceholder'), FAKE_USERNAME_WITHOUT_REPOS); expect(getByPlaceholderText('userSelection.usernamePlaceholder')).toHaveAttribute('value', FAKE_USERNAME_WITHOUT_REPOS); userEvent.click(getByText('userSelection.submitButtonText').closest('button')); getByText('repositories.header'); getByText('repositories.loadingText'); expect(queryByText('repositories.empty')).toBeNull(); await waitForElement(() => getByText('repositories.empty')); expect(queryByText('repositories.error')).toBeNull(); }); });
當用戶輸入不存在的 GitHub 用戶名時,應用程序會顯示錯誤消息。
describe('when GitHub user does not exist', () => { it('user is presented with an error message', async () => { const { getByText, getByPlaceholderText, queryByText } = render(<App />); userEvent.type(getByPlaceholderText('userSelection.usernamePlaceholder'), FAKE_BAD_USERNAME); expect(getByPlaceholderText('userSelection.usernamePlaceholder')).toHaveAttribute('value', FAKE_BAD_USERNAME); userEvent.click(getByText('userSelection.submitButtonText').closest('button')); getByText('repositories.header'); getByText('repositories.loadingText'); expect(queryByText('repositories.empty')).toBeNull(); await waitForElement(() => getByText('repositories.error')); expect(queryByText('repositories.empty')).toBeNull(); }); });
為什麼 React 集成測試很重要
集成測試確實為 React 應用程序提供了一個甜蜜點。 這些測試有助於捕捉錯誤並使用 TDD 方法,同時,它們在實現更改時不需要維護。
本文展示的 React-testing-library 是編寫 React 集成測試的絕佳工具,因為它允許您像用戶一樣與應用程序進行交互,並從用戶的角度驗證應用程序的狀態和行為。
希望這裡提供的示例將幫助您開始在新的和現有的 React 項目上編寫集成測試。 可以在我的 GitHub 上找到包含應用程序實現的完整示例代碼。