跳至主要內容

常見問題

另請參閱主要常見問題,了解非特定於 React 測試的問題。

如何測試輸入框的 onChange 處理函式?

簡而言之:前往 on-change.js 範例

總結

import React from 'react'
import {render, fireEvent} from '@testing-library/react'

test('change values via the fireEvent.change method', () => {
const handleChange = jest.fn()
const {container} = render(<input type="text" onChange={handleChange} />)
const input = container.firstChild
fireEvent.change(input, {target: {value: 'a'}})
expect(handleChange).toHaveBeenCalledTimes(1)
expect(input.value).toBe('a')
})

test('select drop-downs must use the fireEvent.change', () => {
const handleChange = jest.fn()
const {container} = render(
<select onChange={handleChange}>
<option value="1">1</option>
<option value="2">2</option>
</select>,
)
const select = container.firstChild
const option1 = container.getElementsByTagName('option').item(0)
const option2 = container.getElementsByTagName('option').item(1)

fireEvent.change(select, {target: {value: '2'}})

expect(handleChange).toHaveBeenCalledTimes(1)
expect(option1.selected).toBe(false)
expect(option2.selected).toBe(true)
})

test('checkboxes (and radios) must use fireEvent.click', () => {
const handleChange = jest.fn()
const {container} = render(<input type="checkbox" onChange={handleChange} />)
const checkbox = container.firstChild
fireEvent.click(checkbox)
expect(handleChange).toHaveBeenCalledTimes(1)
expect(checkbox.checked).toBe(true)
})

如果您使用過 enzyme 或 React 的 TestUtils,您可能習慣以這樣的方式更改輸入框

input.value = 'a'
Simulate.change(input)

我們無法使用 React 測試函式庫做到這一點,因為 React 實際上會追蹤您每次在 input 上指定 value 屬性的時間,因此當您觸發 change 事件時,React 會認為該值實際上尚未變更。

這對於 Simulate 有效,因為它們使用內部 API 來觸發特殊的模擬事件。使用 React 測試函式庫,我們嘗試避免實作細節,以使您的測試更具彈性。

因此,我們已解決變更事件處理函式以一種 React 無法追蹤的方式為您設定屬性的問題。這就是為什麼您必須將值作為 change 方法呼叫的一部分傳遞的原因。

我可以使用此函式庫編寫單元測試嗎?

當然可以!您可以使用此函式庫編寫單元和整合測試。請參閱下文,了解有關如何模擬依賴關係的更多資訊(因為此函式庫故意不支援淺層渲染),如果您想要對高階元件進行單元測試。此專案中的測試顯示了使用此函式庫進行單元測試的幾個範例。

在您編寫測試時,請記住

您的測試越類似於軟體的使用方式,它們就能給您帶來更多的信心。- 2018 年 2 月 17 日

如何測試元件或 Hook 中拋出的錯誤?

如果元件在渲染期間拋出錯誤,則如果包裝在 act 中,則狀態更新的來源將拋出錯誤。預設情況下,renderfireEvent 包裝在 act 中。您可以將其包裝在 try-catch 中,或者如果您的測試執行器支援這些匹配器,則可以使用專用的匹配器。例如,在 Jest 中,您可以使用 toThrow

function Thrower() {
throw new Error('I throw')
}

test('it throws', () => {
expect(() => render(<Thrower />)).toThrow('I throw')
})

這同樣適用於 Hook 和 renderHook

function useThrower() {
throw new Error('I throw')
}

test('it throws', () => {
expect(() => renderHook(useThrower)).toThrow('I throw')
})
資訊

React 18 將使用擴充的錯誤訊息呼叫 console.error。React 19 將使用擴充的錯誤訊息呼叫 console.warn,除非狀態更新包裝在 act 中。renderrenderHookfireEvent 預設包裝在 act 中。

如果我不能使用淺層渲染,如何模擬測試中的元件?

一般來說,您應該避免模擬元件(請參閱指導原則部分)。但是,如果需要,請嘗試使用 Jest 的模擬功能。我發現模擬特別有用的一個例子是動畫函式庫。我不希望我的測試等待動畫結束。

jest.mock('react-transition-group', () => {
const FakeTransition = jest.fn(({children}) => children)
const FakeCSSTransition = jest.fn(props =>
props.in ? <FakeTransition>{props.children}</FakeTransition> : null,
)
return {CSSTransition: FakeCSSTransition, Transition: FakeTransition}
})

test('you can mock things with jest.mock', () => {
const {getByTestId, queryByTestId} = render(
<HiddenMessage initialShow={true} />,
)
expect(queryByTestId('hidden-message')).toBeTruthy() // we just care it exists
// hide the message
fireEvent.click(getByTestId('toggle-message'))
// in the real world, the CSSTransition component would take some time
// before finishing the animation which would actually hide the message.
// So we've mocked it out for our tests to make it happen instantly
expect(queryByTestId('hidden-message')).toBeNull() // we just care it doesn't exist
})

請注意,由於它們是 Jest 模擬函式 (jest.fn()),您也可以根據需要對它們進行斷言。

開啟完整測試以取得完整範例。

這看起來比淺層渲染需要更多的工作(確實如此),但只要您的模擬與您模擬的內容足夠相似,它就能給您帶來更多的信心。

如果您想要使事情更像淺層渲染,那麼您可以執行更多類似這樣的操作。

從我的部落格文章中了解更多有關 Jest 模擬如何運作的資訊:「但實際上,什麼是 JavaScript 模擬?」

enzyme 的哪些部分「充滿複雜性和功能」並「鼓勵不良的測試實務」?

大多數有害的功能都與鼓勵測試實作細節有關。主要包括淺層渲染、允許按元件建構函式選取渲染元素的 API,以及允許您取得元件實例(及其狀態/屬性)並與之互動的 API(大多數 enzyme 的 wrapper API 都允許這樣做)。

此函式庫的指導原則是

您的測試越類似於軟體的使用方式,它們就能給您帶來更多的信心。- 2018 年 2 月 17 日

由於使用者無法直接與應用程式的元件實例互動、斷言其內部狀態或它們呈現的元件,或呼叫其內部方法,因此在測試中執行這些操作會降低它們能夠給您帶來的信心。

這並不是說永遠沒有執行這些操作的用例,因此應該可以完成這些操作,只是它們不是測試 React 元件的預設和自然方式。

為什麼快照差異不起作用?

如果您使用snapshot-diff函式庫來儲存快照差異,它不會立即運作,因為此函式庫使用可變的 DOM。變更不會傳回新的物件,因此 snapshot-diff 會認為它是相同的物件,並避免進行差異比較。

幸運的是,有一個簡單的方法可以使其運作:在將 DOM 傳遞到 snapshot-diff 時複製 DOM。它看起來像這樣

const firstVersion = container.cloneNode(true)
// Do some changes
snapshotDiff(firstVersion, container.cloneNode(true))
如何修復「更新未包裝在 act(...) 中」的警告?

此警告通常是由於在測試已完成後導致更新的非同步操作引起的。有兩種方法可以解決它

  1. 透過使用非同步公用程式(例如waitForfind* 查詢)之一,在測試中等待操作的結果。例如:const userAddress = await findByLabel(/address/i)
  2. 模擬非同步操作,使其不會觸發狀態更新。

一般而言,首選方法 1,因為它更能符合使用者與您的應用程式互動的期望。

此外,當您考慮如何最好地編寫能讓您有信心並避免這些警告的測試時,您可能會發現此部落格文章很有幫助。

我應該測試元件樹的哪個層級?子項、父項或兩者?

遵循此函式庫的指導原則,將測試組織圍繞使用者體驗和與應用程式功能互動的方式,而不是圍繞特定元件本身,會很有幫助。在某些情況下,例如對於可重複使用的元件函式庫,將開發人員納入要測試的使用者清單,並個別測試每個可重複使用的元件可能會很有用。其他時候,元件樹的特定分解只是一個實作細節,並且個別測試該樹內的每個元件都可能導致問題(請參閱https://kentcdodds.com/blog/avoid-the-test-user)。

在實務上,這意味著通常最好在元件樹中測試較高的層級,以模擬真實的使用者互動。是否值得在此基礎上額外測試較高或較低的層級,這取決於權衡取捨以及什麼將為成本提供足夠的價值(請參閱https://kentcdodds.com/blog/unit-vs-integration-vs-e2e-tests,了解有關不同層級測試的更多資訊)。

如需更深入討論此主題,請參閱此影片