如果元件內有牽涉 HTTP requests ,我們通常需要在測試中 mock 它們。今天將學習如何測試元件的時候 mock 這些 HTTP requests。
我們有個元件叫做 GreetingLoader
,先來看一下元件的程式碼:
這個元件 render 一個 form,form 裡面有一個 id=name
的 input,我們在 input 輸入名稱,按下 button 之後,呼叫 loadGreeting
api 發送 request,得到 response 之後 setGreeting()
,得到的 greeting 文字將 render 在 <div aria-label="greeting"></div>
裡面。
greeting-loader-01-mocking.js
import React from 'react'
import {loadGreeting} from './api'
function GreetingLoader() {
const [greeting, setGreeting] = React.useState('')
async function loadGreetingForInput(e) {
e.preventDefault()
const {data} = await loadGreeting(e.target.elements.name.value)
setGreeting(data.greeting)
}
return (
<form onSubmit={loadGreetingForInput}>
<label htmlFor="name">Name</label>
<input id="name" />
<button type="submit">Load Greeting</button>
<div aria-label="greeting">{greeting}</div>
</form>
)
}
export {GreetingLoader}
現在,我們為 GreetingLoader
這個元件寫一個叫做 'loads greetings on click'
的測試。
'@testing-library/react'
的 render
及 fireEvent
,作為等一下測試元件 render 狀態及元件 click 事件的工具。getByLabelText
測試 label 及關聯的 input 是否 render 在畫面上;使用 getByText
測試 button 是否 render 在畫面上。fireEvent.click()
模擬 button 的 click 事件。以上都是前幾天學習到的內容,我們目前完成這三個步驟的測試程式碼如下:
tests/http-jest-mock.js
import React from 'react'
import {render, fireEvent} from '@testing-library/react'
import {GreetingLoader} from '../greeting-loader-01-mocking'
test('loads greetings on click', () => {
const {getByLabelText, getByText} = render(<GreetingLoader />)
const nameInput = getByLabelText(/name/i)
const loadButton = getByText(/load/i)
nameInput.value = 'Mary'
fireEvent.click(loadButton)
})
回頭看一下 greeting-loader-01-mocking.js
****的程式碼,因為 './api'
的 loadGreeting
會發送 HTTP Request ,但我們並不想在測試中真的這麼做。可以使用 jest.mock
將 './api'
這個 module 整個 mock 起來。
jest.mock
mock module。tests/http-jest-mock.js
import {loadGreeting} from '../api'
jest.mock('../api')
.mockResolvedValueOnce()
mock response 的結果。為了在測試內更明確表示使用的是 mock 版本的 loadGreeting
,我們 import 的時候,可以給它一個 alias: import {loadGreeting as mockLoadGreeting} from '../api'
。
在 mockLoadGreeting.mockResolvedValueOnce()
內傳入 {data: {greeting: 'TEST_GREETING'}}
作為 mock 版本的 response。
tests/http-jest-mock.js
import {loadGreeting as mockLoadGreeting} from '../api' // 使用 alias
jest.mock('../api')
test('loads greetings on click', () => {
mockLoadGreeting.mockResolvedValueOnce({data: {greeting: 'TEST_GREETING'}}) // 傳入 mock 的 response
const {getByLabelText, getByText} = render(<GreetingLoader />)
const nameInput = getByLabelText(/name/i)
const loadButton = getByText(/load/i)
nameInput.value = 'Mary'
fireEvent.click(loadButton)
})
Tips:使用
jest.mock()
mock module,及mockResolvedValueOnce()
mock response
toHaveBeenCalledWith()
測試呼叫時傳入的參數及 toHaveBeenCalledTimes()
測試被呼叫幾次。tests/http-jest-mock.js
test('loads greetings on click', () => {
mockLoadGreeting.mockResolvedValueOnce({data: {greeting: 'TEST_GREETING'}})
const {getByLabelText, getByText} = render(<GreetingLoader />)
const nameInput = getByLabelText(/name/i)
const loadButton = getByText(/load/i)
nameInput.value = 'Mary'
fireEvent.click(loadButton)
expect(mockLoadGreeting).toHaveBeenCalledWith('Mary') // 測試呼叫時傳入的參數
expect(mockLoadGreeting).toHaveBeenCalledTimes(1) // 測試被呼叫幾次
})
這時後,我們跑一下測試,會看到警告訊息:
an update to GreetingLoader inside the test was not wrapped in act
這是 React 提供的警告,直接來看 官方文件 對 act()
的說明:
為了準備讓 component 進行 assert,將 render component 及執行更新的程式碼放在 act() 中。這讓你的測試更貼近 React 在瀏覽器中的運作方式。
根據文件,我們應該把元件的 render 跟更新放進 act()
裡:
it('can render and update a counter', () => {
// Test first render and componentDidMount
act(() => {
ReactDOM.render(<Counter />, container);
});
...
// Test second render and componentDidUpdate
act(() => {
button.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});
...
});
不過,React Testing Library 內部已經幫我們處理好 act()
的部分了,大部分的情況,只要單純使用 React Testing Library 提供的 API ( 例如:render,fireEvent ),不需要自己 wrap act()
。
回到我們的測試例子,出現這個警示是因為:元件內的 HTTP Request 完成之後,執行 setGreeting()
更新 state。這個 state 的更新是非同步的,它發生在 React 的 call stack 之外。簡單來說,就是我們的測試跑完了,但元件其實還沒完成更新。現在我們需要做的就是:「等待」元件更新完成。
React Testing Library 提供 waitFor 這個非同步工具,幫我們處理 mock promises 的等待。
waitFor()
等待元件更新完成,再呼叫 assertions。因為 waitFor
是一個 async function,所以記得在前面加上 await
。
import {render, fireEvent, waitFor} from '@testing-library/react'
test('loads greetings on click', async () => {
mockLoadGreeting.mockResolvedValueOnce({data: {greeting: 'TEST_GREETING'}})
const {getByLabelText, getByText} = render(<GreetingLoader />)
const nameInput = getByLabelText(/name/i)
const loadButton = getByText(/load/i)
nameInput.value = 'Mary'
fireEvent.click(loadButton)
expect(mockLoadGreeting).toHaveBeenCalledWith('Mary')
expect(mockLoadGreeting).toHaveBeenCalledTimes(1)
// 使用 waitFor 等待元件更新
await waitFor(() =>
expect(getByLabelText(/greeting/i)).toHaveTextContent('TEXT_GREETING')
)
})
最後,我們稍微重構整理一下程式碼,將重複的字串 'TEST_GREETING'
抽為變數 testGreeting
。這個測試就算完成了:
import React from 'react'
import {render, fireEvent, waitFor} from '@testing-library/react'
import {loadGreeting as mockLoadGreeting} from '../api'
import {GreetingLoader} from '../greeting-loader-01-mocking'
jest.mock('../api')
test('loads greetings on click', async () => {
const testGreeting = 'TEST_GREETING' // 抽變數
mockLoadGreeting.mockResolvedValueOnce({data: {greeting: testGreeting}})
const {getByLabelText, getByText} = render(<GreetingLoader />)
const nameInput = getByLabelText(/name/i)
const loadButton = getByText(/load/i)
nameInput.value = 'Mary'
fireEvent.click(loadButton)
expect(mockLoadGreeting).toHaveBeenCalledWith('Mary')
expect(mockLoadGreeting).toHaveBeenCalledTimes(1)
await waitFor(() =>
expect(getByLabelText(/greeting/i)).toHaveTextContent(testGreeting),
)
})
Tips:使用
waitFor()
等待非同步的 assertions。