原文鏈接:https://www.robinwieruch.de/react-testing-library
第一次翻譯文章,可能會難以閱讀,建議英文過關的都閱讀原文
Kent C. Dodds的 React Testing Library (RTL) 是Airbnb的 Enzyme 的替代品。Enzyme 為測試 React 組件提供了很多實用的工具,而 React Testing Library(簡稱RTL)則是后退一步並提出疑問:『怎么樣的測試可以讓我們對開發的 React 組件充滿信心?』,相較於測試組件的內部實現細節,RTL把開發者當成一位 React 程序的終端用戶
在這篇React Testing Library教程,我們將會學習怎么對 React 組件進行有信心的單元測試及集成測試。
Jest 和 React Testing Library
React 初學者可能會對React體系的測試工具感到迷惑。React Testing Library和Jest不是非此即彼,而是相互依賴並且都有自己的專屬功能。
在現代的React中,Jest是最熱門的JavaScript程序的測試框架,我們不可避免要去接觸。如果是通過 create-react-app 來創建項目,則 Jest 及 React Testing Library 已經默認安裝了,在package.json
可以看到test script,我們可以通過npm test來運行測試。在此之前,我們先看下面的測試代碼:
describe('my function or component', () => {
test('does the following', () => {
});
});
describe塊是測試套件(test suite),test塊是測試用例(test case),其中test
關鍵字也可以寫成it
。
一個測試套件可以包含多個測試用例,但是一個測試用例不能包含測試套件。
寫在測試用例內部的是斷言(assertions)(例如:Jest的expect
),斷言結果可以是成功,可以是失敗,下面是兩個斷言成功的例子:
describe('true is truthy and false is falsy', () => {
test('true is truthy', () => {
expect(true).toBe(true);
});
test('false is falsy', () => {
expect(false).toBe(false);
});
});
當你把上面的代碼復制到一個test.js
文件中,並且運行npm test
命令,Jest 會自動找到上述代碼並執行。當我們執行npm test
時,Jest測試運行器默認會自動匹配所有test.js
結尾的文件,你可以通過Jest配置文件來配置匹配規則和其他功能。
當你通過Jest測試運行器執行npm test
后,你會看到以下輸出:
在運行所有測試后,你能看到測試用例變為綠色,Jest提供了交互式命令,讓我們可以進一步下達命令。一般而言,Jest會一次性顯示所有的測試結果(對於你的測試用例)。如果你修改了文件(不管源代碼還是測試代碼),Jest都會重新運行所有的測試用例。
function sum(x, y) {
return x + y;
}
describe('sum', () => {
test('sums up two values', () => {
expect(sum(2, 4)).toBe(6);
});
});
在實際開發中,被測試代碼一般與測試代碼在不同的文件,所以需要通過 import 去測試:
import sum from './math.js';
describe('sum', () => {
test('sums up two values', () => {
expect(sum(2, 4)).toBe(6);
});
});
簡而言之,這就是Jest,與任何 React 組件無關。Jest 就是一個通過命令行來提供運行測試的能力的測試運行器。雖然它還提供如『測試套件、測試用例、斷言』等函數,以及其他更加強大的功能,但是本質上上述就是我們需要Jest的原因。
與 Jest 相比,React Testing Library 是一個測試 React 組件的測試庫。另一個熱門的測試庫是之前提到的 Enzyme。下面我們會學習使用 React Testing Library 來測試 React 組件。
RTL:渲染組件
在這個章節,你會學習到怎么通過 RTL 渲染 React 組件。我們會使用 str/App.js 文件下的 App function component:
import React from 'react';
const title = 'Hello React';
function App() {
return <div>{title}</div>;
}
export default App;
在 src/App.test.js 文件添加測試代碼:
import React from 'react';
import { render } from '@testing-library/react';
import App from './App';
describe('App', () => {
test('renders App component', () => {
render(<App />);
});
});
RTL的 render 函數通過 JSX 去渲染內容,然后,你就能在測試代碼中訪問你的組件,通過 RTL 的 debug 函數,可以確保看到渲染的內容:
import React from 'react';
import { render, screen } from '@testing-library/react';
import App from './App';
describe('App', () => {
test('renders App component', () => {
render(<App />);
screen.debug();
});
});
運行 npm test
后,在控制台能夠看到 APP 組件的 HTML 輸出。當你通過 RTL 編寫測試代碼時,都可以先通過 debug
函數查看組件在 RTL 中的渲染結果。這樣可以更高效的編寫代碼。
<body>
<div>
<div>
Hello React
</div>
</div>
</body>
RTL的厲害之處在於,它不關心實際的組件代碼。我們接下來看一下利用了不同特性(useState、event handler,props)和概念(controlled component)的 React 組件:
import React from 'react';
function App() {
const [search, setSearch] = React.useState('');
function handleChange(event) {
setSearch(event.target.value);
}
return (
<div>
<Search value={search} onChange={handleChange}>
Search:
</Search>
<p>Searches for {search ? search : '...'}</p>
</div>
);
}
function Search({ value, onChange, children }) {
return (
<div>
<label htmlFor="search">{children}</label>
<input
id="search"
type="text"
value={value}
onChange={onChange}
/>
</div>
);
}
export default App;
當你執行npm test
后,你能看到 debug 函數有以下輸出:
<body>
<div>
<div>
<div>
<label
for="search"
>
Search:
</label>
<input
id="search"
type="text"
value=""
/>
</div>
<p>
Searches for
...
</p>
</div>
</div>
</body>
RTL 能夠讓你的 React 組件與呈現給人看的時候類似,看到的是React 組件渲染成 HTML,所以你會看到 HTML 結構的輸出,而不是兩個獨立的 React 組件。
RTL:定位元素
在渲染了 React 組件后,RTL 提供了不同的函數去定位元素。定位后的元素可用於『斷言』或者是『用戶交互』。現在我們先來學習,怎么去定位元素:
import React from 'react';
import { render, screen } from '@testing-library/react';
import App from './App';
describe('App', () => {
test('renders App component', () => {
render(<App />);
screen.getByText('Search:');
});
});
當你不清楚 RTL 的 render 函數會渲染出什么時,請保持使用 RTL 的 debug
函數。當你知道渲染的 HTML 的結構后,你才能夠通過 RTL 的 screen
對象的函數進行定位。定位后的元素可用於用戶交互或斷言。我們會檢查元素是否在 DOM 的斷言:
import React from 'react';
import { render, screen } from '@testing-library/react';
import App from './App';
describe('App', () => {
test('renders App component', () => {
render(<App />);
expect(screen.getByText('Search:')).toBeInTheDocument();
});
});
當找不到元素,getByText
函數會拋出一個異常。少數人利用這個特性去使用諸如getByText
的定位函數作為隱式的斷言,通過該函數替代通過 expect
進行顯式的斷言:
import React from 'react';
import { render, screen } from '@testing-library/react';
import App from './App';
describe('App', () => {
test('renders App component', () => {
render(<App />);
// implicit assertion:隱式斷言
screen.getByText('Search:');
// explicit assertion:顯式斷言
// 更推薦該方法
expect(screen.getByText('Search:')).toBeInTheDocument();
});
});
getByText
函數接收一個 string 作為參數,例如我們上面的調用。它也可以接收一個regular expression(正則表達式)作為參數。通過 string 作為參數用於精准匹配,通過 regular expression 可用於部分匹配(模糊匹配),更加便利:
import React from 'react';
import { render, screen } from '@testing-library/react';
import App from './App';
describe('App', () => {
test('renders App component', () => {
render(<App />);
// fails
expect(screen.getByText('Search')).toBeInTheDocument();
// succeeds
expect(screen.getByText('Search:')).toBeInTheDocument();
// succeeds
expect(screen.getByText(/Search/)).toBeInTheDocument();
});
});
getByText
是 RTL 眾多定位函數類型之一,下面我們看下其他的
RTL: 定位類型
你已經學習了 getByText
,其中 Text
是 RTL 中常用語定位元素的一個定位類型,另一個是 getByRole
的 Role
。
getByRole
方法常用於通過 aria-label屬性。但是,HTML 元素可能也會有隱式 role,例如 button元素的button role。因此你不僅可以通過『存在的 text』 來定位元素,也可以通過『可得到的 role』來定位元素。getByRole
有一個巧妙的特性:如果你提供的 role 不存在,它會顯示所有的可選擇的 role。
getByText
和 getByRole
是 RTL 中應用最為廣泛的定位函數。
getByRole
巧妙的特性:如果你提供的 role 不存在於渲染后的HTML,它會顯示所有的可選擇的 role。
import React from 'react';
import { render, screen } from '@testing-library/react';
import App from './App';
describe('App', () => {
test('renders App component', () => {
render(<App />);
screen.getByRole('');
});
});
運行npm test
命令后,它會有以下輸出:
Unable to find an accessible element with the role ""
Here are the accessible roles:
document:
Name "":
<body />
--------------------------------------------------
textbox:
Name "Search:":
<input
id="search"
type="text"
value=""
/>
--------------------------------------------------
由於 HTML 元素的隱式 roles,我們擁有至少一個 textbox(在這是<input />),我們可以通過它使用 getByRole
進行定位
import React from 'react';
import { render, screen } from '@testing-library/react';
import App from './App';
describe('App', () => {
test('renders App component', () => {
render(<App />);
expect(screen.getByRole('textbox')).toBeInTheDocument();
});
});
因為 DOM 已經給 HTML 附加了隱式 roles,所以一般情況下我們不需要為了測試而在 HTML 元素中顯式指定 aria roles 。這就是 getByRole
成為 getByText
在 RTL 的定位函數中的有力競爭者。
還有其他更特定元素的查詢類型:
- LabelText: getByLabelText: <label for="search" />
- PlaceholderText: getByPlaceholderText: <input placeholder="Search" />
- AltText: getByAltText: <img alt="profile" />
- DisplayValue: getByDisplayValue: <input value="JavaScript" />
還有一種不得已的情況下使用的定位類型, TestId
的 getByTestId
函數需要在源代碼中添加 data-testid
屬性才能使用。畢竟一般而言,getByText
和 getByRole
應該是你定位元素的首選定位類型。
- getByText
- getByRole
- getByLabelText
- getByPlaceholderText
- getByAltText
- getByDisplayValue
最后強調一遍,以上這些都是 RTL 的不同的定位類型。
RTL:定位的變異種類(VARIANTS)
與定位類型相比,也存在定位的變異種類(簡稱變種)。其中一個定位變種是 RTL 中 getByText
和 getByRole
使用的 getBy
變種,這也是測試 React 組件時,默認使用的定位變種。
另外兩個定位變種是 queryBy
和 findBy
,它們都可以通過 getBy
有的定位類型進行擴展。例如,queryBy
擁有以下的定位類型:
- queryByText
- queryByRole
- queryByLabelText
- queryByPlaceholderText
- queryByAltText
- queryByDisplayValue
而 findBy
則擁有以下定位類型:
- findByText
- findByRole
- findByLabelText
- findByPlaceholderText
- findByAltText
- findByDisplayValue
getBy
和 queryBy
有什么不同?
現在面臨一個問題:什么時候使用 getBy
,什么時候使用其他兩個變種 queryBy
和 findBy
。你已經知道 getBy
在無法定位元素時,會拋出一個異常。這是一個便利的副作用,因為這可以讓開發者更早地注意到測試代碼中發生了某些錯誤。但是,這也會導致在斷言時一些不應該發生的異常:
import React from 'react';
import { render, screen } from '@testing-library/react';
import App from './App';
describe('App', () => {
test('renders App component', () => {
render(<App />);
screen.debug();
// fails
expect(screen.getByText(/Searches for JavaScript/)).toBeNull();
});
});
例子中的斷言失敗了,通過 debug
函數的輸出我們得知:因為 getBy
在當前HTML中找不到文本 "Searches for JavaScript" ,所以 getBy
在我們進行斷言之前拋出了一個異常。為了驗證某個元素不在頁面中,我們改用 queryBy
來替代 getBy
:
import React from 'react';
import { render, screen } from '@testing-library/react';
import App from './App';
describe('App', () => {
test('renders App component', () => {
render(<App />);
expect(screen.queryByText(/Searches for JavaScript/)).toBeNull();
});
});
所以,當你想要驗證一個元素不在頁面中,使用 queryBy
,否則使用 getBy
。那么什么時候使用 findBy
呢?
什么時候使用 findBy
findBy
變體用於那些最終會顯示在頁面當中的異步元素,我們創建一個新的 React 組件來說明該場景:
function getUser() {
return Promise.resolve({ id: '1', name: 'Robin' });
}
function App() {
const [search, setSearch] = React.useState('');
const [user, setUser] = React.useState(null);
React.useEffect(() => {
const loadUser = async () => {
const user = await getUser();
setUser(user);
};
loadUser();
}, []);
function handleChange(event) {
setSearch(event.target.value);
}
return (
<div>
{user ? <p>Signed in as {user.name}</p> : null}
<Search value={search} onChange={handleChange}>
Search:
</Search>
<p>Searches for {search ? search : '...'}</p>
</div>
);
}
該組件首次渲染后,App 組件會 fetches 一個用於通過模擬的 API,該 API 返回一個立馬 resolves 為 user 對象的 JavaScript promise 對象,並且組件以 React 組件 State 的方式存儲來自 promise 的 user。在組件更新並重新渲染之后,由於條件渲染的原因,會在組件中渲染出文本:"Signed in as"。
如果我們要測試組件從第一次渲染到第二次渲染的過程中,promise 被 resolved,我們需要寫一個異步的測試因為我們必須等待 promise 對象被異步 resolve。換句話說,我們需要等待 user 對象在組件更新一次后重新渲染:
import React from 'react';
import { render, screen } from '@testing-library/react';
import App from './App';
describe('App', () => {
test('renders App component', async () => {
render(<App />);
expect(screen.queryByText(/Signed in as/)).toBeNull();
expect(await screen.findByText(/Signed in as/)).toBeInTheDocument();
});
});
在組件的初始化渲染中,我們在 HTML 中無法通過 queryBy
找到 "Signed in as"(這里使用 queryBy
代替了 getBy
), 然后,我們 await 一個新的元素被找到,並且最終確實被找到當 promise resolves 並且組件重新渲染之后。
如果你不相信這個結果,可以添加兩個 debug
函數並在命令行驗證它們的輸出:
import React from 'react';
import { render, screen } from '@testing-library/react';
import App from './App';
describe('App', () => {
test('renders App component', async () => {
render(<App />);
expect(screen.queryByText(/Signed in as/)).toBeNull();
screen.debug();
expect(await screen.findByText(/Signed in as/)).toBeInTheDocument();
screen.debug();
});
});
對於任何開始不顯示、但遲早會顯示的元素,使用 findBy
取代 getBy
及 queryBy
。如果你想要驗證一個元素不在頁面中,使用 queryBy
,否則默認使用 getBy
。
如果是多個元素怎么辦
我們已經學習了三個定位變種:getBy
,queryBy
,findBy
。這些都能與定位類型關聯(例如:Text,Role,PlaceholderText,DisplayValue)。但這些定位函數只返回一個變量,那怎么驗證返回多個變量的情況呢(例如:React 組件中的 list)?
所有的定位變種都可以通過 All
關鍵字擴展:
- getAllBy
- queryAllBy
- findAllBy
它們都會返回一個元素的數組並且可以再次通過定位類型進行定位。(原文是這么說的,但實際使用中似乎不能)
斷言函數
斷言函數出現在斷言過程的右邊。在上一個例子中,你已經使用了兩個斷言函數:toBeNull
和 toBeInTheDocument
。它們都是 RTL 中主要被用於檢查元素是否顯示在頁面中的函數。
通常所有的斷言函數都來自 Jest 。但是, RTL 通過自己的實現擴展了已有的 API ,例如 toBeInTheDocument
函數。所有這些來自一個額外包 的擴展的函數都已經在你使用 create-react-app
創建項目時自動設置。
- toBeDisabled
- toBeEnabled
- toBeEmpty
- toBeEmptyDOMElement
- toBeInTheDocument
- toBeInvalid
- toBeRequired
- toBeValid
- toBeVisible
- toContainElement
- toContainHTML
- toHaveAttribute
- toHaveClass
- toHaveFocus
- toHaveFormValues
- toHaveStyle
- toHaveTextContent
- toHaveValue
- toHaveDisplayValue
- toBeChecked
- toBePartiallyChecked
- toHaveDescription
RTL:Fire Event
目前為止,我們只學習了通過 getBy
(或 queryBy
)測試 React 組件的元素渲染,以及擁有條件渲染元素的 React 組件的再渲染。但是實際的用戶交互是怎么樣的呢?當用戶想 input
輸入文字,組件可能會再渲染(例如我們的例子),或者一個新的值會被顯示(或者被使用在任何地方)。
我們可以通過 RTL 的 fireEvent
函數去模擬終端用戶的交互。下面我們學習一下:
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import App from './App';
describe('App', () => {
test('renders App component', () => {
render(<App />);
screen.debug();
fireEvent.change(screen.getByRole('textbox'), {
target: { value: 'JavaScript' },
});
screen.debug();
});
});
fireEvent
函數需要兩個參數,一個參數是定位的元素(例子中使用 textbox role 定位 input
),另一個參數是event
(例子中的 event
擁有屬性 value
,value
的值為 "JavaScript")。debug
方法可以顯示 fireEvent
執行前后顯示的 HTML 結構的差別,然后你就能夠看到 input
的字段值被重新渲染了。
另外,如果你的組件擁有一個異步的任務,例如我們的 APP 組件需要 fetches 一個 user,你可能會看到一條警告:"Warning: An update to App inside a test was not wrapped in act(...).(翻譯:警告:測試代碼中,一個更新 APP 的操作沒有被 act 函數包含)";這個警告的意思是,當存在一些異步任務執行時,我們需要去處理它。一般而言這個能夠通過 RTL 的 act
函數解決,但在這個例子中,我們只需要等待 user 被 resolve:
describe('App', () => {
test('renders App component', async () => {
render(<App />);
// wait for the user to resolve
// needs only be used in our special case
await screen.findByText(/Signed in as/);
screen.debug();
fireEvent.change(screen.getByRole('textbox'), {
target: { value: 'JavaScript' },
});
screen.debug();
});
});
然后,我們可以在 fireEvent
發生前后進行斷言:
describe('App', () => {
test('renders App component', async () => {
render(<App />);
// wait for the user to resolve
// needs only be used in our special case
await screen.findByText(/Signed in as/);
expect(screen.queryByText(/Searches for JavaScript/)).toBeNull();
fireEvent.change(screen.getByRole('textbox'), {
target: { value: 'JavaScript' },
});
expect(screen.getByText(/Searches for JavaScript/)).toBeInTheDocument();
});
});
我們在 fireEvent
執行前,通過 queryBy
定位變種去檢查元素不顯示,在 fireEvent
執行后,通過 getBy
定位變種去檢查元素顯示。 有時候你也會看到一些文章在最后一次斷言會使用 queryBy
,因為 queryBy
與 getBy
在斷言元素存在於頁面上時,用法比較類似。
當然,fireEvent
除了可以解決測試中異步的行為,還可以直接使用並進行斷言
RTL:用戶事件(User Event)
RTL 還擁有一個擴展用戶行為的庫,該庫通過 fireEvent API 進行擴展。上面我們已經通過 fireEvent
去觸發用戶交互;下面我們會使用 userEvent
去替代它,因為 userEvent
的 API 相比於 fireEvent
的API 更真實地模仿了瀏覽器行為。例如: fireEvent.change()
函數觸發了瀏覽器的 change
事件,但是 userEvent.type
觸發了瀏覽器的 chagnge
,keyDown
,keyPress
,keyUp
事件。
import React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import App from './App';
describe('App', () => {
test('renders App component', async () => {
render(<App />);
// wait for the user to resolve
await screen.findByText(/Signed in as/);
expect(screen.queryByText(/Searches for JavaScript/)).toBeNull();
await userEvent.type(screen.getByRole('textbox'), 'JavaScript');
expect(
screen.getByText(/Searches for JavaScript/)
).toBeInTheDocument();
});
});
任何情況下,優先使用 userEvent
而不是 fireEvent
。雖然寫該文章的時候,userEvent
還不完全包含 fireEvent
的所有特性,但是,以后的事誰知道呢。
RTL:處理回調
有時候你會對 React 組件單獨進行單元測試。這些組件一般不會擁有副作用及狀態,只接收 props
和返回 JSX
或處理callback handlers
。我們已經知道怎么測試接收 props
和 component
的 JSX 渲染了。下面我們會對 Search 組件的 callback hanlers 測試:
function Search({ value, onChange, children }) {
return (
<div>
<label htmlFor="search">{children}</label>
<input
id="search"
type="text"
value={value}
onChange={onChange}
/>
</div>
);
}
渲染及斷言我們之前已經看過了。這次我們看一下通過 Jest 的工具去 mock
一個 onChange
函數並傳遞給組件。 然后通過 input
輸入框觸發用戶交互,我們就可以去驗證 onChange
方法被調用:
describe('Search', () => {
test('calls the onChange callback handler', () => {
const onChange = jest.fn();
render(
<Search value="" onChange={onChange}>
Search:
</Search>
);
fireEvent.change(screen.getByRole('textbox'), {
target: { value: 'JavaScript' },
});
// 注意,這里斷言觸發回調了 1 次
expect(onChange).toHaveBeenCalledTimes(1);
});
});
再一次說明,userEvent
相比於 fireEvent
更貼近用戶在瀏覽器的表現,當 fireEvent
觸發 change
事件只調用了一次回調函數,但 userEvent
對每個字母的輸入都會觸發回調:
describe('Search', () => {
test('calls the onChange callback handler', async () => {
const onChange = jest.fn();
render(
<Search value="" onChange={onChange}>
Search:
</Search>
);
await userEvent.type(screen.getByRole('textbox'), 'JavaScript');
// 這里斷言觸發回調了 10 次
expect(onChange).toHaveBeenCalledTimes(10);
});
});
但是,RTL 不建議你過多地孤立地測試你的組件(傻瓜組件),而是應該更多的與其他組件進行集成測試。只有這樣你才能更好的測試出 state
改變對 DOM 的影響,以及是否有副作用發生。
RTL:Asynchronous / Async
在前面的例子中,我們看到例子通過 async
await
及 findBy
去等待定位一些開始不在,但最后一定會顯示出來的元素來進行測試。現在我們通過一個小示例來測試 React 中 fetching 數據。以下是通過 axios 遠程 fetching 數據的 React 組件:
import React from 'react';
import axios from 'axios';
const URL = 'http://hn.algolia.com/api/v1/search';
function App() {
const [stories, setStories] = React.useState([]);
const [error, setError] = React.useState(null);
async function handleFetch(event) {
let result;
try {
result = await axios.get(`${URL}?query=React`);
setStories(result.data.hits);
} catch (error) {
setError(error);
}
}
return (
<div>
<button type="button" onClick={handleFetch}>
Fetch Stories
</button>
{error && <span>Something went wrong ...</span>}
<ul>
{stories.map((story) => (
<li key={story.objectID}>
<a href={story.url}>{story.title}</a>
</li>
))}
</ul>
</div>
);
}
export default App;
點擊按鈕,我們從 Hacker News API 獲取了 stories 數組。當順利獲取數據,React 會在頁面渲染出一個 stories 列表,否則,我們將會看到一個異常提示。App 組件的測試會是以下這樣的:
import React from 'react';
import axios from 'axios';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import App from './App';
jest.mock('axios');
describe('App', () => {
test('fetches stories from an API and displays them', async () => {
const stories = [
{ objectID: '1', title: 'Hello' },
{ objectID: '2', title: 'React' },
];
axios.get.mockImplementationOnce(() =>
Promise.resolve({ data: { hits: stories } })
);
render(<App />);
await userEvent.click(screen.getByRole('button'));
const items = await screen.findAllByRole('listitem');
expect(items).toHaveLength(2);
});
});
在 render
App 組件之前,我們確保對 API 進行mocked。以上例子中,axios 通過 get
方法返回數據,所以我們對它進行了mocked。但是如果你使用其他庫或者瀏覽器的 fetch API 獲取數據,你也需要對此進行mocked。
對 API mock 並渲染組件后,我們使用 userEvent
點擊按鈕去觸發 API 請求。但是由於請求是異步的,我們需要等待組件進行更新。之前我們使用 RTL 的 findBy
定位變種去等待終將出現的元素。
import React from 'react';
import axios from 'axios';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import App from './App';
jest.mock('axios');
describe('App', () => {
test('fetches stories from an API and displays them', async () => {
...
});
test('fetches stories from an API and fails', async () => {
axios.get.mockImplementationOnce(() =>
Promise.reject(new Error())
);
render(<App />);
await userEvent.click(screen.getByRole('button'));
const message = await screen.findByText(/Something went wrong/);
expect(message).toBeInTheDocument();
});
});
最后一段測試代碼展示了:怎么進行測試 React 組件的 API 異常的場景。我們通過 reject
promise 來代替 resolves
promise 來進行 mock。並在渲染組件后,進行模擬點擊,最后我們看到頁面展示一個異常消息:
import React from 'react';
import axios from 'axios';
import { render, screen, act } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import App from './App';
jest.mock('axios');
describe('App', () => {
test('fetches stories from an API and displays them', async () => {
const stories = [
{ objectID: '1', title: 'Hello' },
{ objectID: '2', title: 'React' },
];
const promise = Promise.resolve({ data: { hits: stories } });
axios.get.mockImplementationOnce(() => promise);
render(<App />);
await userEvent.click(screen.getByRole('button'));
await act(() => promise);
expect(screen.getAllByRole('listitem')).toHaveLength(2);
});
test('fetches stories from an API and fails', async () => {
...
});
});
為了完整起見,最后的例子向你展示了怎么以更明確的方式 await promise,通過 act
函數而不是等待 HTML 出現在頁面。
使用 RTL 去測試 React 的異步行為不是一件困難的事情。你已經在測試代碼中通過使用 Jest 去 mock 了外部模塊(remote API),並且 await 數據及重新渲染了 React 組件。
React Testing Library 是我用於測試 React 組件的測試庫。之前我在任何時候都會使用 Airbnb 的 Enzyme,不過我很喜歡 RTL 引導你去關注用戶行為而不是實現細節。你可以通過編寫類似於真實用戶使用般的測試代碼去測試你的程序可用性。