作者選擇Vets Who Code作為Write for Donations計畫的捐贈對象。
簡介
獲得穩固的測試覆蓋率對於建立對您的 Web 應用程式的信心至關重要。 Jest 是一個 JavaScript 測試運行器,提供了撰寫和執行測試的資源。 React Testing Library 提供了一組測試助手,根據使用者互動而不是組件的實現細節來結構化您的測試。 Jest 和 React Testing Library 都預先打包在Create React App 中,並遵循測試應用程式應該與軟體的使用方式相似的指導原則。
在本教程中,您將在包含各種 UI 元素的示例專案中測試異步代碼和交互作用。 您將使用 Jest 撰寫和執行單元測試,並實施 React Testing Library 作為輔助 DOM(Document Object Model)庫來處理與組件的交互作用。
先決條件
完成此教程,您需要:
-
Node.js版本14或更高版本已安裝在您的本地計算機上。要在macOS或Ubuntu 18.04上安裝Node.js,請按照在macOS上安裝Node.js並創建本地開發環境中的步驟或使用PPA進行安裝的部分進行操作如何在Ubuntu 18.04上安裝Node.js。
-
在您的本地计算机上安装
npm
版本 5.2 或更高版本,您需要在示例项目中使用 Create React App 和npx
。如果您没有同时安装npm
和Node.js
,请立即执行此操作。对于 Linux,使用命令sudo apt install npm
。- 为了使本教程中的
npm
包正常工作,请安装build-essential
包。对于 Linux,使用命令sudo apt install build-essential
。
- 为了使本教程中的
-
在您的本地计算机上安装 Git。您可以检查 Git 是否已在您的计算机上安装,或者通过 在 Ubuntu 20.04 上安装 Git 操作系统的安装过程进行安装。
-
熟悉React,您可以通过如何在React.js中编码系列进行开发。因为示例项目已经使用Create React App进行了引导,您无需单独安装它。
-
对于作为测试运行器或框架的Jest有一些熟悉会有所帮助,但不是必需的。因为Jest已经预先打包在Create React App中,您无需单独安装它。
步骤1 — 设置项目
在這個步驟中,您將克隆一個示例項目並啟動測試套件。示例項目使用了三個主要工具:Create React App、Jest 和 React Testing Library。Create React App 用於啟動單頁 React 應用程序。Jest 用作測試運行器,而 React Testing Library 提供了用於結構化測試的輔助工具,圍繞用戶交互展開。
首先,您將從 GitHub 克隆一個預建的 React App。您將與名為Doggy Directory的應用程序一起工作,這是一個使用Dog API構建的示例項目,用於基於特定品種的狗的集合構建搜索和顯示系統。
要從 GitHub 克隆該項目,請打開終端並運行以下命令:
您將看到類似於以下的輸出:
OutputCloning into 'doggy-directory'...
remote: Enumerating objects: 64, done.
remote: Counting objects: 100% (64/64), done.
remote: Compressing objects: 100% (48/48), done.
remote: Total 64 (delta 21), reused 55 (delta 15), pack-reused 0
Unpacking objects: 100% (64/64), 228.16 KiB | 3.51 MiB/s, done.
切換到doggy-directory
文件夾:
安裝項目依賴:
npm install
命令將安裝package.json
文件中定義的所有項目依賴。
安裝完依賴後,您可以查看應用程序的部署版本,或者您可以運行以下命令在本地運行應用程序:
如果選擇在本地運行應用程序,它將在http://localhost:3000/
上打開。您將在終端中看到以下輸出:
OutputCompiled successfully!
You can now view doggy-directory in the browser.
Local: http://localhost:3000
On Your Network: http://network_address:3000
啟動後,應用程序的首頁將如下所示:
項目的依賴項已安裝,應用程序現在正在運行。接下來,打開一個新的終端並使用以下命令啟動測試:
使用Jest作為測試運行器,npm test
命令以交互式觀察模式啟動測試。在觀察模式下,文件更改後測試會自動重新運行。測試將在您更改文件時運行並通知您該更改是否通過測試。
首次運行npm test
後,您將在終端中看到以下輸出:
OutputNo tests found related to files changed since last commit.
Press `a` to run all tests, or run Jest with `--watchAll`.
Watch Usage
› Press a to run all tests.
› Press f to run only failed tests.
› Press q to quit watch mode.
› Press p to filter by a filename regex pattern.
› Press t to filter by a test name regex pattern.
› Press Enter to trigger a test run.
現在您已經運行了示例應用程序和測試套件,您可以開始測試登陸頁面。
步驟2 — 測試登陸頁面
默認情況下,Jest將尋找帶有.test.js
後綴的文件以及帶有.js
後綴的文件在__tests__
文件夾中。當您對相關的測試文件進行更改時,它們將被自動檢測到。隨著測試用例的修改,輸出將自動更新。針對doggy-directory
示例項目準備的測試文件已在添加測試範式之前設置了最小代碼。在此步驟中,您將編寫測試以驗證應用程序的登陸頁面在執行搜索之前將會加載。
在編輯器中打開src/App.test.js
以查看以下代碼:
A minimum of one test block is required in each test file. Each test block accepts two required parameters: the first argument is a string representing the name of the test case; the second argument is a function that holds the expectations of the test.
在函數內部,React Testing Library 提供了一個 render
方法,用於將你的組件渲染到 DOM 中。將要測試的組件渲染到測試環境的 DOM 中後,你現在可以開始編寫代碼來 斷言 預期的功能。
你將在 render
方法中添加一個測試塊,測試在進行任何 API 調用或選擇之前,登錄頁面是否準確渲染。在 render
方法下面添加下面突出顯示的代碼:
expect
函數用於每次你想要驗證某個結果時,它接受一個參數,表示你的代碼產生的值。大多數 expect
函數都與 匹配器 函數配對,用於斷言關於特定值的某些事情。對於大多數這些斷言,你將使用 jest-dom 提供的附加匹配器,以便更輕鬆地檢查 DOM 中常見的方面。例如,.toHaveTextContent
是第一行中 expect
函數的匹配器,而 getByRole("heading")
是用於獲取 DOM 元素的選擇器。
React Testing Library 提供了 screen
對象,作為訪問所需查詢以對測試 DOM 環境進行斷言的便捷方式。默認情況下,React Testing Library 提供了允許你在 DOM 中定位元素的查詢。有三個主要類別的查詢:
getBy*
(最常用)queryBy*
(在不引发错误的情况下用于测试元素的不存在)findBy*
(用于测试异步代码时使用)
每种查询类型都有特定的目的,稍后在教程中将定义。在此步骤中,您将专注于最常见的查询类型getBy*
。要查看不同查询变体的详尽列表,您可以查看React的查询速查表。
以下是Doggy Directory登陆页面的注释图像,指示第一个测试(渲染登陆页面)涵盖的每个部分:
每个expect
函数都针对以下内容进行断言(如上面的注释图像中所示):
- 您期望具有heading role的元素具有子字符串匹配Doggy Directory。
- 您期望选择输入具有选择品种的确切显示值。
- 您期望搜索按钮被禁用,因为尚未进行选择。
- 您期望占位图像存在于文档中,因为尚未进行搜索。
當完成後,請保存src/App.test.js
文件。因為測試正在監視模式下運行,更改將自動註冊。如果更改沒有自動註冊,您可能需要停止並重新啟動測試套件。
現在,當您在終端中查看測試時,您將看到以下輸出:
Output PASS src/App.test.js
✓ renders the landing page (172 ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 2.595 s, estimated 5 s
Ran all test suites related to changed files.
Watch Usage: Press w to show more.
在這一步驟中,您編寫了一個初始測試來驗證Doggy Directory登陸頁面的初始渲染視圖。在下一步中,您將學習如何模擬用於測試異步代碼的API調用。
第3步 – 模擬fetch
方法
在這一步驟中,您將回顧一種模擬JavaScript的fetch
方法的方法。雖然有很多種方法可以實現這一點,但這個實現將使用Jest的spyOn
和mockImplementation
方法。
當您依賴外部API時,有可能它們的API會掛掉或花費一段時間才能返回響應。模擬fetch
方法提供了一個一致且可預測的環境,讓您對測試更有信心。API模擬機制對於正確運行使用外部API的測試是必要的。
注意:為了讓這個項目保持簡化,您將模擬fetch方法。然而,建議您在模擬大型、可生產的代碼庫中的異步代碼時使用更健壯的解決方案,例如模擬服務器工作者(MSW)。
打開src/mocks/mockFetch.js
以查看mockFetch
方法的工作原理:
mockFetch
方法返回一個對象,其結構與應用程序中的API調用返回的fetch
調用的結構非常相似。在Doggy Directory應用程序中測試異步功能的兩個區域都需要mockFetch
方法:填充品種列表的選擇下拉菜單和當執行搜索時檢索狗圖像的API調用。
關閉src/mocks/mockFetch.js
。現在您了解了在測試中將如何使用mockFetch
方法,您可以將其導入測試文件。將mockFetch
函數作為參數傳遞給mockImplementation
方法,然後將其用作fetch API的虛擬實現。
在src/App.test.js
中,添加突出顯示的代碼行以導入mockFetch
方法:
此代碼將設置和拆卸模擬實現,以便每個測試都從一個平等的基礎開始。
jest.spyOn(window, "fetch");
創建一個模擬函數,將跟踪附加到DOM全局window變量的fetch
方法的調用。
.mockImplementation(mockFetch);
接受一个函数,该函数将被用于实现模拟方法。因为这个命令覆盖了原始的 fetch
实现,所以它会在应用代码中调用 fetch
时运行。
完成后,请保存 src/App.test.js
文件。
现在,当您在终端中查看测试时,您将收到以下输出:
Output console.error
Warning: An update to App inside a test was not wrapped in act(...).
When testing, code that causes React state updates should be wrapped into act(...):
act(() => {
/* fire events that update state */
});
/* assert on the output */
This ensures that you're testing the behavior the user would see in the browser. Learn more at https://reactjs.org/link/wrap-tests-with-act
at App (/home/sammy/doggy-directory/src/App.js:5:31)
18 | })
19 | .then((json) => {
> 20 | setBreeds(Object.keys(json.message));
| ^
21 | });
22 | }, []);
23 |
...
PASS src/App.test.js
✓ renders the landing page (429 ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 1.178 s, estimated 2 s
Ran all test suites related to changed files.
警告告诉您发生了意外的状态更新。然而,输出还表明测试成功模拟了 fetch
方法。
在这一步中,您模拟了 fetch
方法,并将该方法纳入了测试套件。虽然测试通过了,但您仍然需要解决警告。
步骤 4 — 修复 act
警告
在这一步中,您将学习如何解决在步骤 3 中更改后出现的 act
警告。
act
警告是因为您模拟了 fetch
方法,而当组件挂载时,它会发出一个 API 调用来获取品种列表。品种列表存储在一个状态变量中,该变量填充了选择输入中的 option
元素。
下面的图片显示了在成功进行 API 调用以填充品种列表后选择输入的样子:
警告是因为在测试块完成渲染组件后设置状态。
要解决此问题,请将以下突出显示的修改添加到src/App.test.js
中的测试用例中:
async
关键字告诉Jest,异步代码是由组件挂载时发生的API调用引起的。
A new assertion with the findBy
query verifies that the document contains an option with the value of husky
. findBy
queries are used when you need to test asynchronous code that is dependent on something being in the DOM after a period of time. Because the findBy
query returns a promise that gets resolved when the requested element is found in the DOM, the await
keyword is used within the expect
method.
完成后,请保存对src/App.test.js
的更改。
有了这些新的添加,现在你会发现在你的测试中act
警告不再存在:
Output PASS src/App.test.js
✓ renders the landing page (123 ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 0.942 s, estimated 2 s
Ran all test suites related to changed files.
Watch Usage: Press w to show more.
在这一步中,你学会了如何修复处理异步代码时可能出现的act
警告。接下来,你将添加第二个测试用例来验证Doggy Directory应用程序的交互功能。
步骤 5 — 测试搜索功能
在最后一步中,你将编写一个新的测试用例来验证搜索和图像显示功能。你将利用各种查询和API方法来实现正确的测试覆盖率。
返回您的編輯器中的src/App.test.js
文件。在文件頂部,使用以下突出顯示的命令將user-event
伴侶庫和waitForElementToBeRemoved
異步方法導入測試文件:
您將在本節後面使用這些導入。
在初始test()
方法之後,添加一個新的異步測試塊,並使用以下代碼塊渲染App
組件:
組件渲染後,您現在可以添加驗證Doggy Directory應用程序的交互功能的函數。
仍然在src/App.test.js
中,在第二個test()
方法中添加突出顯示的代碼塊:
上面突出顯示的部分將模擬選擇一個狗品種並驗證顯示的正確值。
getByRole
查詢抓取所選元素並將其分配給select
變量。
與您在第4步中修復act
警告的方式類似,使用findByRole
查詢來等待cattledog
選項出現在文檔中,然後再進行進一步的斷言。
userEvent
對象之前引入的將模擬常見的用戶交互。在這個例子中,selectOptions
方法選擇了在前一行中等待的cattledog
選項。
最後一行斷言select
變量包含上面選擇的cattledog
值。
你將要添加到Javascript test()
塊中的下一個部分將初始化搜索請求,以根據所選品種查找狗的圖像,並確認是否存在加載狀態。
添加突出顯示的行:
getByRole
查詢定位搜索按鈕並將其分配給searchBtn
變量。
toBeDisabled
jest-dom匹配器將驗證在進行品種選擇時搜索按鈕未被禁用。
click
方法在userEvent
對象上模擬單擊搜索按鈕。
先前引入的waitForElementToBeRemoved
异步助手函数将在搜索API调用进行中等待Loading消息的出现和消失。 waitForElementToBeRemoved
回调中的queryByText
检查元素的缺失而不会抛出错误。
下面的图片显示了在搜索进行中显示的加载状态:
接下来,添加以下Javascript代码来验证图片和结果计数的显示:
getAllByRole
查询将选择所有狗图片并将它们分配给dogImages
变量。查询的*AllBy*
变体返回一个包含匹配指定角色的多个元素的数组。 *AllBy*
变体不同于ByRole
变体,后者只能返回单个元素。
模拟的fetch
实现在响应中包含了两个图像URL。通过Jest的toHaveLength
匹配器,您可以验证显示了两个图像。
getByText
查詢將檢查右上角是否顯示正確的結果計數。
使用 toHaveAccessibleName
匹配器的兩個斷言將驗證個別圖像是否關聯有適當的替代文字。
A completed search displaying images of the dog based on the breed selected along with the number of results found will look like this:
將新的 JavaScript 代碼的所有部分結合後,App.test.js
檔案將如下所示:
保存在 src/App.test.js
中所做的更改。
當您審查您的測試時,終端中的最終輸出將會是以下內容:
Output PASS src/App.test.js
✓ renders the landing page (273 ms)
✓ should be able to search and display dog image results (123 ms)
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 4.916 s
Ran all test suites related to changed files.
Watch Usage: Press w to show more.
在這最後一步中,您添加了一個測試來驗證 Doggy Directory 應用程序的搜索、加載和顯示功能。通過最後的斷言撰寫,您現在知道您的應用程序是有效的。
結論
在本教程中,您使用了 Jest、React Testing Library 和 jest-dom 匹配器编写了测试用例。逐步构建,您根据用户与 UI 的交互编写了测试。您还学习了 getBy*
、findBy*
和 queryBy*
查询之间的区别,以及如何测试异步代码。
要了解更多有关上述主题的信息,请查看 Jest、React Testing Library 和 jest-dom 官方文档。您还可以阅读 Kent C. Dodd 的 Common Mistakes with React Testing Library,了解在使用 React Testing Library 时的最佳实践。有关在 React 应用程序中使用快照测试的更多信息,请参阅 How To Write Snapshot Tests。