iT邦幫忙

第 12 屆 iT 邦幫忙鐵人賽

DAY 19
1
Modern Web

循序漸進學習 Javascript 測試系列 第 19

Day 19 測試 React 元件:Mock HTTP Requests

如果元件內有牽涉 HTTP requests ,我們通常需要在測試中 mock 它們。今天將學習如何測試元件的時候 mock 這些 HTTP requests。

使用 jest.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'renderfireEvent ,作為等一下測試元件 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

  • 步驟六:爲 mock function 加入 assertions。使用 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。


上一篇
Day 18 測試 React 元件:測試元件的 Event Handlers
下一篇
Day 20 測試 React 元件:測試 Error Boundary 元件
系列文
循序漸進學習 Javascript 測試30

尚未有邦友留言

立即登入留言