用Jest來給React完成一次妙趣橫生的~單元測試

引言

在2020的今天,構建一個 web 應用對於咱們來講,並不是什麼難事。由於有不少足夠多優秀的的前端框架(好比 ReactVue 和 Angular);以及一些易用且強大的UI庫(好比 Ant Design)爲咱們保駕護航,極大地縮短了應用構建的週期。html

可是,互聯網時代也急劇地改變了許多軟件設計,開發和發佈的方式。開發者面臨的問題是,需求愈來愈多,應用愈來愈複雜,時不時會有一種失控的的感受,並在心中大喊一句:「我太南了!」。嚴重的時候甚至會出現我改了一行代碼,卻不清楚其影響範圍狀況。這種時候,就須要測試的方式,來保障咱們應用的質量和穩定性了。前端

接下來,讓咱們學習下,如何給 React 應用寫單元測試吧🎁node

須要什麼樣的測試

軟件測試是有級別的,下面是《Google軟件測試之道》一書中,對於測試認證級別的定義,摘錄以下:react

級別1ios

使用測試覆蓋率工具。使用持續集成。測試分級爲小型、中型、大型。建立冒煙測試集合(主流程測試用例)。標記哪些測試是非肯定性的測試(測試結果不惟一)。git

級別2github

若是有測試運行結果爲紅色(失敗❌)就不會發布。每次代碼提交以前都要求經過冒煙測試。(自測,簡單走下主流程)各類類型的總體代碼覆蓋率要大於50%。小型測試的覆蓋率要大於10%。web

級別3npm

全部重要的代碼變動都要通過測試。小型測試的覆蓋率大於50%。新增重要功能都要經過集成測試的驗證。redux

級別4

在提交任何新代碼以前都會自動運行冒煙測試。冒煙測試必須在30分鐘內運行完畢。沒有不肯定性的測試。整體測試覆蓋率應該不小於40%。小型測試的代碼覆蓋率應該不小於25%。全部重要的功能都應該被集成測試驗證到。

級別5

對每個重要的缺陷修復都要增長一個測試用例與之對應。積極使用可用的代碼分析工具。整體測試覆蓋率不低於60%。小型測試代碼覆蓋率應該不小於40%。





小型測試,一般也叫單元測試,通常來講都是自動化實現的。用於驗證一個單獨的函數,組件,獨立功能模塊是否能夠按照預期的方式運行。

而對於開發者來講,重要的是進行了測試的動做。本篇文章主要圍繞着React組件單元測試展開的,其目的是爲了讓開發人員能夠站在使用者的角度考慮問題。經過測試的手段,確保組件的每個功能均可以正常的運行,關注質量,而不是讓用戶來幫你測試。

在編寫單元測試的時候,必定會對以前的代碼反覆進行調整,雖然過程比較痛苦,可組件的質量,也在一點一點的提升。

技術棧選擇

當咱們想要爲 React 應用編寫單元測試的時候,官方推薦是使用 React Testing Library[1] + Jest[2] 的方式。Enzyme[3] 也是十分出色的單元測試庫,咱們應該選擇哪一種測試工具呢?

下面讓咱們看一個簡單的計數器的例子,以及兩個相應的測試:第一個是使用 Enzyme[4] 編寫的,第二個是使用 React Testing Library[5] 編寫的。

counter.js

// counter.jsimport React from "react";
class Counter extends React.Component { state = { count: 0 }; increment = () => this.setState(({ count }) => ({ count: count + 1 })); decrement = () => this.setState(({ count }) => ({ count: count - 1 })); render() { return ( <div> <button onClick={this.decrement}>-</button> <p>{this.state.count}</p> <button onClick={this.increment}>+</button> </div> ); }}
export default Counter;

counter-enzyme.test.js

// counter-enzyme.test.jsimport React from "react";import { shallow } from "enzyme";
import Counter from "./counter";
describe("<Counter />", () => { it("properly increments and decrements the counter", () => { const wrapper = shallow(<Counter />); expect(wrapper.state("count")).toBe(0);
wrapper.instance().increment(); expect(wrapper.state("count")).toBe(1);
wrapper.instance().decrement(); expect(wrapper.state("count")).toBe(0); });});

counter-rtl.test.js

// counter-rtl.test.jsimport React from "react";import { render, fireEvent } from "@testing-library/react";
import Counter from "./counter";
describe("<Counter />", () => { it("properly increments and decrements the counter", () => { const { getByText } = render(<Counter />); const counter = getByText("0"); const incrementButton = getByText("+"); const decrementButton = getByText("-");
fireEvent.click(incrementButton); expect(counter.textContent).toEqual("1");
fireEvent.click(decrementButton); expect(counter.textContent).toEqual("0"); });});

比較兩個例子,你能看出哪一個測試文件是最好的嘛?若是你不是很熟悉單元測試,可能會任務兩種都很好。可是實際上 Enzyme 的實現有兩個誤報的風險:

即便代碼損壞,測試也會經過。即便代碼正確,測試也會失敗。

讓咱們來舉例說明這兩點。假設您但願重構組件,由於您但願可以設置任何count值。所以,您能夠刪除遞增和遞減方法,而後添加一個新的setCount方法。假設你忘記將這個新方法鏈接到不一樣的按鈕:

counter.js

// counter.jsexport default class Counter extends React.Component { state = { count: 0 }; setCount = count => this.setState({ count }); render() { return ( <div> <button onClick={this.decrement}>-</button> <p>{this.state.count}</p> <button onClick={this.increment}>+</button> </div> ); }}

第一個測試(Enzyme)將經過,但第二個測試(RTL)將失敗。實際上,第一個並不關心按鈕是否正確地鏈接到方法。它只查看實現自己,也就是說,您的遞增和遞減方法執行以後,應用的狀態是否正確。這就是代碼損壞,測試也會經過

如今是2020年,你也許據說過 React Hooks,而且打算使用 React Hooks 來改寫咱們的計數器代碼:

counter.js

// counter.jsimport React, { useState } from "react";
export default function Counter() { const [count, setCount] = useState(0); const increment = () => setCount(count => count + 1); const decrement = () => setCount(count => count - 1); return ( <div> <button onClick={decrement}>-</button> <p>{count}</p> <button onClick={increment}>+</button> </div> );}

這一次,即便您的計數器仍然工做,第一個測試也將被打破。Enzyme 會報錯,函數組件中沒法使用state:

ShallowWrapper::state() can only be called on class components

接下來,就須要改寫單元測試文件了:

counter-enzyme.test.js

import React from "react";import { shallow } from "enzyme";
import Counter from "./counter";
describe("<Counter />", () => { it("properly increments and decrements the counter", () => { const setValue = jest.fn(); const useStateSpy = jest.spyOn(React, "useState"); useStateSpy.mockImplementation(initialValue => [initialValue, setValue]); const wrapper = shallow(<Counter />);
wrapper .find("button") .last() .props() .onClick(); expect(setValue).toHaveBeenCalledWith(1); // We can't make any assumptions here on the real count displayed // In fact, the setCount setter is mocked!
wrapper .find("button") .first() .props() .onClick(); expect(setValue).toHaveBeenCalledWith(-1); });});

而使用 React Testing Library 編寫的單元測試仍是能夠正常運行的,由於它更加關注應用的事件處理,以及展現;而非應用的實現細節,以及狀態變化。更加符合咱們對於單元測試的本來訴求,以及最佳實踐。

可遵循的簡單規則

也許上文中使用 React Testing Library 編寫的單元測試示例,還會給人一種一頭霧水的感受。下面,讓咱們使用 AAA 模式來一步一步的拆解這部分代碼。

AAA模式:編排(Arrange),執行(Act),斷言(Assert)。

幾乎全部的測試都是這樣寫的。首先,您要編排(初始化)您的代碼,以便爲接下來的步驟作好一切準備。而後,您執行用戶應該執行的步驟(例如單擊)。最後,您對應該發生的事情進行斷言

import React from "react";import { render, fireEvent } from "@testing-library/react";
import Counter from "./app";
describe("<Counter />", () => { it("properly increments the counter", () => { // Arrange const { getByText } = render(<Counter />); const counter = getByText("0"); const incrementButton = getByText("+"); const decrementButton = getByText("-");
// Act fireEvent.click(incrementButton); // Assert expect(counter.textContent).toEqual("1");
// Act fireEvent.click(decrementButton); // Assert expect(counter.textContent).toEqual("0"); });});

編排(Arrange)

在編排這一步,咱們須要完成2項任務:

渲染組件獲取所需的DOM的不一樣元素。

渲染組件可使用 RTL's API 的 render 方法完成。簽名以下:

function render( ui: React.ReactElement, options?: Omit<RenderOptions, 'queries'>): RenderResult

ui 是你要加載的組件。options 一般不須要指定選項。官方文檔在這裏[6],若是要指定的話,以下值是對官方文檔的簡單摘錄:

container:React Testing庫將建立一個div並將該div附加到文檔中。而經過這個參數,能夠自定義容器。baseElement:若是指定了容器,則此值默認爲該值,不然此值默認爲document.documentElement。這將用做查詢的基本元素,以及在使用debug()時打印的內容。hydrate:用於服務端渲染,使用 ReactDOM.hydrate 加載你的組件。wrapper:傳遞一個組件做爲包裹層,將咱們要測試的組件渲染在其中。這一般用於建立能夠重用的自定義 render 函數,以便提供經常使用數據。queries:查詢綁定。除非合併,不然將覆蓋DOM測試庫中的默認設置。

基本上,這個函數所作的就是使用ReactDOM呈現組件。在直接附加到document.body的新建立的div中呈現(或爲服務器端呈現提供水合物)。所以,能夠從DOM測試庫和其餘一些有用的方法(如debug、rerender或unmount)得到大量查詢。

文檔:https://testing-library.com/docs/dom-testing-library/api-queries#queries

但你可能會想,這些問題是什麼呢?有些實用程序容許您像用戶那樣查詢DOM:經過標籤文本、佔位符和標題查找元素。如下是一些來自文檔的查詢示例:

getByLabelText:搜索與做爲參數傳遞的給定文本匹配的標籤,而後查找與該標籤關聯的元素。getByText:搜索具備文本節點的全部元素,其中的textContent與做爲參數傳遞的給定文本匹配。getByTitle:返回具備與做爲參數傳遞的給定文本匹配的title屬性的元素。getByPlaceholderText:搜索具備佔位符屬性的全部元素,並找到與做爲參數傳遞的給定文本相匹配的元素。

一個特定的查詢有不少變體:

getBy:返回查詢的第一個匹配節點,若是沒有匹配的元素或找到多個匹配,則拋出一個錯誤。getAllBy:返回一個查詢中全部匹配節點的數組,若是沒有匹配的元素,則拋出一個錯誤。queryBy:返回查詢的第一個匹配節點,若是沒有匹配的元素,則返回null。這對於斷言不存在的元素很是有用。queryAllBy:返回一個查詢的全部匹配節點的數組,若是沒有匹配的元素,則返回一個空數組([])。findBy:返回一個promise,該promise將在找到與給定查詢匹配的元素時解析。若是未找到任何元素,或者在默認超時時間爲4500毫秒後找到了多個元素,則承諾將被拒絕。findAllBy:返回一個promise,當找到與給定查詢匹配的任何元素時,該promise將解析爲元素數組。

執行(Act)

如今一切都準備好了,咱們能夠行動了。爲此,咱們大部分時間使用了來自DOM測試庫的fireEvent,其簽名以下:

fireEvent(node: HTMLElement, event: Event)

簡單地說,這個函數接受一個DOM節點(您可使用上面看到的查詢查詢它!)並觸發DOM事件,如單擊、焦點、更改等。您能夠在這裏找到許多其餘能夠調度的事件。

咱們的例子至關簡單,由於咱們只是想點擊一個按鈕,因此咱們只需:

fireEvent.click(incrementButton);// ORfireEvent.click(decrementButton);

斷言(Assert)

接下來是最後一部分。觸發事件一般會觸發應用程序中的一些更改,所以咱們必須執行一些斷言來確保這些更改發生。在咱們的測試中,這樣作的一個好方法是確保呈現給用戶的計數已經更改。所以,咱們只需斷言textContent屬性的計數器是遞增或遞減:

expect(counter.textContent).toEqual("1");expect(counter.textContent).toEqual("0");

恭喜你,到這裏你已經將咱們的示例拆解成功。🥳

注意:這個AAA模式並不特定於測試庫。事實上,它甚至是任何測試用例的通常結構。我在這裏向您展現這個是由於我發現測試庫如何方便地在每一個部分中編寫測試是一件頗有趣的事情。

8個典型的例子

到這裏,就進入實戰階段了,接下來請先下載示例:rts-guide-demo[7] 。

安裝依賴的同時能夠簡單看下咱們的項目。src/test 目錄下存放了全部單元測試相關的文件。讓咱們清空這個文件夾,再將下面的示例依次手過一遍。🙏(CV也是能夠的👌)

1.如何建立測試快照

快照,顧名思義,容許咱們保存給定組件的快照。當您進行更新或重構,並但願獲取或比較更改時,它會提供不少幫助。

如今,讓咱們看一下 App.js 文件的快照。

App.test.js

import React from 'react'import {render, cleanup} from '@testing-library/react'import App from '../App'
afterEach(cleanup)
it('should take a snapshot', () => { const { asFragment } = render(<App />)
expect(asFragment()).toMatchSnapshot()})

要獲取快照,咱們首先必須導入 render 和 cleanup 。這兩種方法將在本文中大量使用。

render,顧名思義,有助於渲染React組件。cleanup 做爲一個參數傳遞給 afterEach ,以便在每次測試後清理全部東西,以免內存泄漏。

接下來,咱們可使用 render 呈現App組件,並從方法中獲取 asFragment 做爲返回值。最後,確保App組件的片斷與快照匹配。

如今,要運行測試,打開您的終端並導航到項目的根目錄,並運行如下命令:

npm test

所以,它將建立一個新的文件夾 __snapshots__ 和一個文件 App.test.js:

App.test.js.snap

// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`should take a snapshot 1`] = `<DocumentFragment> <div class="App" > <h1> Testing Updated </h1> </div></DocumentFragment>`;

若是,你在 App.js 中作出更改,測試將失敗,由於快照將再也不匹配。更新快照能夠按 u ,或者將對應快照文件刪除便可。

2.測試DOM元素

要測試DOM元素,首先必須查看TestElements.js文件。

TestElements.js

import React from 'react'
const TestElements = () => { const [counter, setCounter] = React.useState(0)
return ( <> <h1 data-testid="counter">{ counter }</h1> <button data-testid="button-up" onClick={() => setCounter(counter + 1)}> Up</button> <button disabled data-testid="button-down" onClick={() => setCounter(counter - 1)}>Down</button> </> ) }
export default TestElements

在這裏,您惟一須要保留的是 data-testid 。它將用於從測試文件中選擇這些元素。如今,讓咱們完成單元測試:

測試計數器是否爲0,以及按鈕的禁用狀態:

TestElements.test.js

import React from 'react';import "@testing-library/jest-dom/extend-expect";import { render, cleanup } from '@testing-library/react';import TestElements from '../components/TestElements'
afterEach(cleanup);
it('should equal to 0', () => { const { getByTestId } = render(<TestElements />); expect(getByTestId('counter')).toHaveTextContent(0) });
it('should be enabled', () => { const { getByTestId } = render(<TestElements />); expect(getByTestId('button-up')).not.toHaveAttribute('disabled') });
it('should be disabled', () => { const { getByTestId } = render(<TestElements />); expect(getByTestId('button-down')).toBeDisabled() });

正如您所看到的,語法與前面的測試很是類似。惟一的區別是,咱們使用 getByTestId 選擇必要的元素(根據 data-testid )並檢查是否經過了測試。換句話說,咱們檢查 <h1 data-testid="counter">{ counter }</h1> 中的文本內容是否等於0。

這裏,像往常同樣,咱們使用 getByTestId 選擇元素和檢查第一個測試若是按鈕禁用屬性。對於第二個,若是按鈕是否被禁用。

若是您保存文件或在終端紗線測試中再次運行,測試將經過。

3.測試事件

在編寫單元測試以前,讓咱們首先看下 TestEvents.js 是什麼樣子的。

import React from 'react'
const TestEvents = () => { const [counter, setCounter] = React.useState(0)
return ( <> <h1 data-testid="counter">{ counter }</h1> <button data-testid="button-up" onClick={() => setCounter(counter + 1)}> Up</button> <button data-testid="button-down" onClick={() => setCounter(counter - 1)}>Down</button> </> ) }
export default TestEvents

如今,讓咱們編寫測試。

當咱們點擊按鈕時,測試計數器的增減是否正確:

import React from 'react';import "@testing-library/jest-dom/extend-expect";import { render, cleanup, fireEvent } from '@testing-library/react';import TestEvents from '../components/TestEvents'
afterEach(cleanup);
it('increments counter', () => { const { getByTestId } = render(<TestEvents />);
fireEvent.click(getByTestId('button-up'))
expect(getByTestId('counter')).toHaveTextContent('1') });
it('decrements counter', () => { const { getByTestId } = render(<TestEvents />);
fireEvent.click(getByTestId('button-down'))
expect(getByTestId('counter')).toHaveTextContent('-1') });

能夠看到,除了預期的文本內容以外,這兩個測試很是類似。

第一個測試使用 fireEvent.click() 觸發一個 click 事件,檢查單擊按鈕時計數器是否增長到1。

第二個檢查當點擊按鈕時計數器是否減爲-1。

fireEvent 有幾個能夠用來測試事件的方法,所以您能夠自由地深刻文檔瞭解更多信息。

如今咱們已經知道了如何測試事件,接下來咱們將在下一節中學習如何處理異步操做。

4. 測試異步操做

異步操做是須要時間才能完成的操做。它能夠是HTTP請求、計時器等等。

如今,讓咱們檢查 TestAsync.js 文件。

import React from 'react'
const TestAsync = () => { const [counter, setCounter] = React.useState(0)
const delayCount = () => ( setTimeout(() => { setCounter(counter + 1) }, 500) )
return ( <> <h1 data-testid="counter">{ counter }</h1> <button data-testid="button-up" onClick={delayCount}> Up</button> <button data-testid="button-down" onClick={() => setCounter(counter - 1)}>Down</button> </> ) }
export default TestAsync

這裏,咱們使用 setTimeout() 將遞增事件延遲0.5秒。

測試計數器在0.5秒後判斷是否增長:

TestAsync.test.js

import React from 'react';import "@testing-library/jest-dom/extend-expect";import { render, cleanup, fireEvent, waitForElement } from '@testing-library/react';import TestAsync from '../components/TestAsync'
afterEach(cleanup);
it('increments counter after 0.5s', async () => { const { getByTestId, getByText } = render(<TestAsync />);
fireEvent.click(getByTestId('button-up'))
const counter = await waitForElement(() => getByText('1'))
expect(counter).toHaveTextContent('1')
});

要測試遞增事件,咱們首先必須使用 async/await 來處理操做,由於如前所述,完成它須要時間。

接下來,咱們使用一個新的助手方法 getByText()。這相似於getByTestId()getByText()選擇文本內容,而不是id。

如今,在單擊按鈕以後,咱們等待 waitForElement(() => getByText('1') 來增長計數器。一旦計數器增長到1,咱們如今能夠移動到條件並檢查計數器是否等於1。

也就是說,如今讓咱們轉向更復雜的測試用例。

你準備好了嗎?

5.測試 React Redux

讓咱們檢查一下 TestRedux.js 是什麼樣子的。

TestRedux.js

import React from 'react'import { connect } from 'react-redux'
const TestRedux = ({counter, dispatch}) => {
const increment = () => dispatch({ type: 'INCREMENT' }) const decrement = () => dispatch({ type: 'DECREMENT' })
return ( <> <h1 data-testid="counter">{ counter }</h1> <button data-testid="button-up" onClick={increment}>Up</button> <button data-testid="button-down" onClick={decrement}>Down</button> </> ) }
export default connect(state => ({ counter: state.count }))(TestRedux)

store/reducer.js

export const initialState = { count: 0, }
export function reducer(state = initialState, action) { switch (action.type) { case 'INCREMENT': return { count: state.count + 1, } case 'DECREMENT': return { count: state.count - 1, } default: return state } }

正如您所看到的,沒有什麼特別的。它只是一個由 React Redux 處理的基本計數器組件。

如今,讓咱們來編寫單元測試。

測試初始狀態是否爲0:

import React from 'react'import "@testing-library/jest-dom/extend-expect";import { createStore } from 'redux'import { Provider } from 'react-redux'import { render, cleanup, fireEvent } from '@testing-library/react';import { initialState, reducer } from '../store/reducer'import TestRedux from '../components/TestRedux'
const renderWithRedux = ( component, { initialState, store = createStore(reducer, initialState) } = {}) => { return { ...render(<Provider store={store}>{component}</Provider>), store, }}
afterEach(cleanup);
it('checks initial state is equal to 0', () => { const { getByTestId } = renderWithRedux(<TestRedux />) expect(getByTestId('counter')).toHaveTextContent('0') })
it('increments the counter through redux', () => { const { getByTestId } = renderWithRedux(<TestRedux />, {initialState: {count: 5} }) fireEvent.click(getByTestId('button-up')) expect(getByTestId('counter')).toHaveTextContent('6') })
it('decrements the counter through redux', () => { const { getByTestId} = renderWithRedux(<TestRedux />, { initialState: { count: 100 }, }) fireEvent.click(getByTestId('button-down')) expect(getByTestId('counter')).toHaveTextContent('99') })

咱們須要導入一些東西來測試 React Redux 。這裏,咱們建立了本身的助手函數 renderWithRedux() 來呈現組件,由於它將被屢次使用。

renderWithRedux() 做爲參數接收要呈現的組件、初始狀態和存儲。若是沒有存儲,它將建立一個新的存儲,若是它沒有接收初始狀態或存儲,它將返回一個空對象。

接下來,咱們使用render()來呈現組件並將存儲傳遞給提供者。

也就是說,咱們如今能夠將組件 TestRedux 傳遞給 renderWithRedux() 來測試計數器是否等於0。

測試計數器的增減是否正確:

爲了測試遞增和遞減事件,咱們將初始狀態做爲第二個參數傳遞給renderWithRedux()。如今,咱們能夠單擊按鈕並測試預期的結果是否符合條件。

如今,讓咱們進入下一節並介紹 React Context。

6. 測試 React Context

讓咱們檢查一下 TextContext.js 是什麼樣子的。

import React from "react"
export const CounterContext = React.createContext()
const CounterProvider = () => { const [counter, setCounter] = React.useState(0) const increment = () => setCounter(counter + 1) const decrement = () => setCounter(counter - 1)
return ( <CounterContext.Provider value={{ counter, increment, decrement }}> <Counter /> </CounterContext.Provider> )}
export const Counter = () => { const { counter, increment, decrement } = React.useContext(CounterContext) return ( <> <h1 data-testid="counter">{ counter }</h1> <button data-testid="button-up" onClick={increment}> Up</button> <button data-testid="button-down" onClick={decrement}>Down</button> </> )}
export default CounterProvider

如今,經過 React Context 管理計數器狀態。讓咱們編寫單元測試來檢查它是否按預期運行。

測試初始狀態是否爲0:

TextContext.test.js

import React from 'react'import "@testing-library/jest-dom/extend-expect";import { render, cleanup, fireEvent } from '@testing-library/react'import CounterProvider, { CounterContext, Counter } from '../components/TestContext'
const renderWithContext = ( component) => { return { ...render( <CounterProvider value={CounterContext}> {component} </CounterProvider>) }}
afterEach(cleanup);
it('checks if initial state is equal to 0', () => { const { getByTestId } = renderWithContext(<Counter />) expect(getByTestId('counter')).toHaveTextContent('0')})
it('increments the counter', () => { const { getByTestId } = renderWithContext(<Counter />)
fireEvent.click(getByTestId('button-up')) expect(getByTestId('counter')).toHaveTextContent('1') })
it('decrements the counter', () => { const { getByTestId} = renderWithContext(<Counter />)
fireEvent.click(getByTestId('button-down')) expect(getByTestId('counter')).toHaveTextContent('-1') })

與前面的React Redux部分同樣,這裏咱們使用相同的方法,建立一個助手函數renderWithContext()來呈現組件。可是這一次,它只接收做爲參數的組件。爲了建立新的上下文,咱們將CounterContext傳遞給 Provider。

如今,咱們能夠測試計數器最初是否等於0。那麼,計數器的增減是否正確呢?

正如您所看到的,這裏咱們觸發一個 click 事件來測試計數器是否正確地增長到1並減小到-1。

也就是說,咱們如今能夠進入下一節並介紹React Router。

7. 測試 React Router

讓咱們檢查一下 TestRouter.js 是什麼樣子的。

TestRouter.js

import React from 'react'
import { Link, Route, Switch, useParams } from 'react-router-dom'
const About = () => <h1>About page</h1>
const Home = () => <h1>Home page</h1>
const Contact = () => { const { name } = useParams()
return <h1 data-testid="contact-name">{name}</h1>}
const TestRouter = () => { const name = 'John Doe' return ( <> <nav data-testid="navbar"> <Link data-testid="home-link" to="/">Home</Link> <Link data-testid="about-link" to="/about">About</Link> <Link data-testid="contact-link" to={`/contact/${name}`}>Contact</Link> </nav>
<Switch> <Route exact path="/" component={Home} /> <Route path="/about" component={About} /> <Route path="/about:name" component={Contact} /> </Switch> </> )}
export default TestRouter

這裏,將測試路由對應的頁面信息是否正確。

TestRouter.test.js

import React from 'react'import "@testing-library/jest-dom/extend-expect";import { Router } from 'react-router-dom'import { render, fireEvent } from '@testing-library/react'import { createMemoryHistory } from 'history'import TestRouter from '../components/TestRouter'

const renderWithRouter = (component) => { const history = createMemoryHistory() return { ...render ( <Router history={history}> {component} </Router> ) }}
it('should render the home page', () => {
const { container, getByTestId } = renderWithRouter(<TestRouter />) const navbar = getByTestId('navbar') const link = getByTestId('home-link')
expect(container.innerHTML).toMatch('Home page') expect(navbar).toContainElement(link)})
it('should navigate to the about page', ()=> { const { container, getByTestId } = renderWithRouter(<TestRouter />)
fireEvent.click(getByTestId('about-link'))
expect(container.innerHTML).toMatch('About page') })
it('should navigate to the contact page with the params', ()=> { const { container, getByTestId } = renderWithRouter(<TestRouter />)
fireEvent.click(getByTestId('contact-link'))
expect(container.innerHTML).toMatch('John Doe') })

要測試React Router,咱們首先必須有一個導航歷史記錄。所以,咱們使用 createMemoryHistory() 來建立導航歷史。

接下來,咱們使用助手函數 renderWithRouter() 來呈現組件,並將歷史記錄傳遞給路由器組件。這樣,咱們如今就能夠測試在開始時加載的頁面是不是主頁。以及導航欄是否加載了預期的連接。

測試當咱們點擊連接時,它是否用參數導航到其餘頁面:

如今,要檢查導航是否工做,咱們必須觸發導航連接上的單擊事件。

對於第一個測試,咱們檢查內容是否等於About頁面中的文本,對於第二個測試,咱們測試路由參數並檢查它是否正確經過。

如今咱們能夠進入最後一節,學習如何測試Axios請求。

8. 測試HTTP請求

讓咱們檢查一下 TestRouter.js 是什麼樣子的。

import React from 'react'import axios from 'axios'
const TestAxios = ({ url }) => { const [data, setData] = React.useState()
const fetchData = async () => { const response = await axios.get(url) setData(response.data.greeting) }
return ( <> <button onClick={fetchData} data-testid="fetch-data">Load Data</button> { data ? <div data-testid="show-data">{data}</div>: <h1 data-testid="loading">Loading...</h1> } </> )
}
export default TestAxios

正如您在這裏看到的,咱們有一個簡單的組件,它有一個用於發出請求的按鈕。若是數據不可用,它將顯示一個加載消息。

如今,讓咱們編寫測試。

來驗證數據是否正確獲取和顯示:

TextAxios.test.js

import React from 'react'import "@testing-library/jest-dom/extend-expect";import { render, waitForElement, fireEvent } from '@testing-library/react'import axiosMock from 'axios'import TestAxios from '../components/TestAxios'
jest.mock('axios')
it('should display a loading text', () => {
const { getByTestId } = render(<TestAxios />)
expect(getByTestId('loading')).toHaveTextContent('Loading...')})
it('should load and display the data', async () => { const url = '/greeting' const { getByTestId } = render(<TestAxios url={url} />)
axiosMock.get.mockResolvedValueOnce({ data: { greeting: 'hello there' }, })
fireEvent.click(getByTestId('fetch-data'))
const greetingData = await waitForElement(() => getByTestId('show-data'))
expect(axiosMock.get).toHaveBeenCalledTimes(1) expect(axiosMock.get).toHaveBeenCalledWith(url) expect(greetingData).toHaveTextContent('hello there')})

這個測試用例有點不一樣,由於咱們必須處理HTTP請求。爲此,咱們必須在jest.mock('axios')的幫助下模擬axios請求。

如今,咱們可使用axiosMock並對其應用get()方法。最後,咱們將使用Jest函數mockResolvedValueOnce()來傳遞模擬數據做爲參數。

如今,對於第二個測試,咱們能夠單擊按鈕來獲取數據並使用async/await來解析它。如今咱們要測試三件事:

若是HTTP請求已經正確完成若是使用url完成了HTTP請求若是獲取的數據符合指望。

對於第一個測試,咱們只檢查加載消息在沒有數據要顯示時是否顯示。

也就是說,咱們如今已經完成了八個簡單的步驟來測試你的React應用程序。

更多例子請參考React Testing Library官方文檔[8]

結語

React Testing Library 是用於測試 React 應用的一大利器。它爲咱們提供了訪問 jest-dom 匹配器的機會,以及最佳實踐,使得咱們可使用它來更有效地測試咱們的組件。但願這篇文章是有用的,它將幫助您在將來構建更加健壯的 React 應用程序。

參考文章

React 官方文檔[9]React Testing Library 官方文檔[10]How to Start Testing Your React Apps Using the React Testing Library and Jest[11]Test React apps with React Testing Library[12]

References

[1] React Testing Library: https://testing-library.com/docs/react-testing-library/intro
[2] Jest: https://jestjs.io/
[3] Enzyme: https://github.com/enzymejs/enzyme
[4] Enzyme: https://github.com/enzymejs/enzyme
[5] React Testing Library: https://testing-library.com/docs/react-testing-library/intro
[6] 官方文檔在這裏: https://testing-library.com/docs/react-testing-library/api#render-options
[7] rts-guide-demo: https://github.com/jokingzhang/rts-guide-demo
[8] React Testing Library官方文檔: https://testing-library.com/docs/example-input-event
[9] React 官方文檔: https://zh-hans.reactjs.org/docs/testing.html
[10] React Testing Library 官方文檔: https://testing-library.com/docs/example-input-event
[11] How to Start Testing Your React Apps Using the React Testing Library and Jest: https://www.freecodecamp.org/news/8-simple-steps-to-start-testing-react-apps-using-react-testing-library-and-jest/
[12] Test React apps with React Testing Library: https://thomlom.dev/test-react-testing-library/


後記

若是你、喜歡探討技術,或者對本文有任何的意見或建議,你能夠掃描下方二維碼,關注微信公衆號「 魚頭的Web海洋 」,隨時與魚頭互動。歡迎!衷心但願能夠碰見你。


本文分享自微信公衆號 - 魚頭的Web海洋(krissarea)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索