這篇文章主要講解了“怎么使用React Testing Library和Jest完成單元測試”,文中的講解內(nèi)容簡單清晰,易于學習與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學習“怎么使用React Testing Library和Jest完成單元測試”吧!
創(chuàng)新互聯(lián)堅持“要么做到,要么別承諾”的工作理念,服務(wù)領(lǐng)域包括:成都網(wǎng)站制作、成都網(wǎng)站建設(shè)、外貿(mào)營銷網(wǎng)站建設(shè)、企業(yè)官網(wǎng)、英文網(wǎng)站、手機端網(wǎng)站、網(wǎng)站推廣等服務(wù),滿足客戶于互聯(lián)網(wǎng)時代的瑞麗網(wǎng)站設(shè)計、移動媒體設(shè)計的需求,幫助企業(yè)找到有效的互聯(lián)網(wǎng)解決方案。努力成為您成熟可靠的網(wǎng)絡(luò)建設(shè)合作伙伴!
技術(shù)棧選擇
當我們想要為 React 應(yīng)用編寫單元測試的時候,官方推薦是使用 React Testing Library + Jest 的方式。Enzyme 也是十分出色的單元測試庫,我們應(yīng)該選擇哪種測試工具呢?
下面讓我們看一個簡單的計數(shù)器的例子,以及兩個相應(yīng)的測試:第一個是使用 Enzyme 編寫的,第二個是使用 React Testing Library 編寫的。
counter.js
// counter.js import 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 (); } } export default Counter;{this.state.count}
counter-enzyme.test.js
// counter-enzyme.test.js import React from "react"; import { shallow } from "enzyme"; import Counter from "./counter"; describe("", () => { it("properly increments and decrements the counter", () => { const wrapper = shallow( ); 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.js import React from "react"; import { render, fireEvent } from "@testing-library/react"; import Counter from "./counter"; describe("", () => { it("properly increments and decrements the counter", () => { const { getByText } = render( ); 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"); }); });
比較兩個例子,你能看出哪個測試文件是最好的嘛?如果你不是很熟悉單元測試,可能會任務(wù)兩種都很好。但是實際上 Enzyme 的實現(xiàn)有兩個誤報的風險:
即使代碼損壞,測試也會通過。
即使代碼正確,測試也會失敗。
讓我們來舉例說明這兩點。假設(shè)您希望重構(gòu)組件,因為您希望能夠設(shè)置任何count值。因此,您可以刪除遞增和遞減方法,然后添加一個新的setCount方法。假設(shè)你忘記將這個新方法連接到不同的按鈕:
counter.js
// counter.js export default class Counter extends React.Component { state = { count: 0 }; setCount = count => this.setState({ count }); render() { return (); } }{this.state.count}
第一個測試(Enzyme)將通過,但第二個測試(RTL)將失敗。實際上,第一個并不關(guān)心按鈕是否正確地連接到方法。它只查看實現(xiàn)本身,也就是說,您的遞增和遞減方法執(zhí)行之后,應(yīng)用的狀態(tài)是否正確。
這就是代碼損壞,測試也會通過。
現(xiàn)在是2020年,你也許聽說過 React Hooks,并且打算使用 React Hooks 來改寫我們的計數(shù)器代碼:
counter.js
// counter.js import 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 (); }{count}
這一次,即使您的計數(shù)器仍然工作,第一個測試也將被打破。Enzyme 會報錯,函數(shù)組件中無法使用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("", () => { 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( ); 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 編寫的單元測試還是可以正常運行的,因為它更加關(guān)注應(yīng)用的事件處理,以及展示;而非應(yīng)用的實現(xiàn)細節(jié),以及狀態(tài)變化。更加符合我們對于單元測試的原本訴求,以及最佳實踐。
可遵循的簡單規(guī)則
也許上文中使用 React Testing Library 編寫的單元測試示例,還會給人一種一頭霧水的感覺。下面,讓我們使用 AAA 模式來一步一步的拆解這部分代碼。
AAA模式:編排(Arrange),執(zhí)行(Act),斷言(Assert)。
幾乎所有的測試都是這樣寫的。首先,您要編排(初始化)您的代碼,以便為接下來的步驟做好一切準備。然后,您執(zhí)行用戶應(yīng)該執(zhí)行的步驟(例如單擊)。最后,您對應(yīng)該發(fā)生的事情進行斷言。
import React from "react"; import { render, fireEvent } from "@testing-library/react"; import Counter from "./app"; describe("", () => { it("properly increments the counter", () => { // Arrange const { getByText } = render( ); 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項任務(wù):
渲染組件
獲取所需的DOM的不同元素。
渲染組件可以使用 RTL's API 的 render 方法完成。簽名如下:
function render( ui: React.ReactElement, options?: Omit): RenderResult
ui 是你要加載的組件。options 通常不需要指定選項。官方文檔在這里,如果要指定的話,如下值是對官方文檔的簡單摘錄:
container:React Testing庫將創(chuàng)建一個div并將該div附加到文檔中。而通過這個參數(shù),可以自定義容器。
baseElement:
如果指定了容器,則此值默認為該值,否則此值默認為document.documentElement。這將用作查詢的基本元素,以及在使用debug()時打印的內(nèi)容。
hydrate:用于服務(wù)端渲染,使用 ReactDOM.hydrate 加載你的組件。
wrapper:傳遞一個組件作為包裹層,將我們要測試的組件渲染在其中。這通常用于創(chuàng)建可以重用的自定義 render 函數(shù),以便提供常用數(shù)據(jù)。
queries:查詢綁定。除非合并,否則將覆蓋DOM測試庫中的默認設(shè)置。
這里,像往常一樣,我們使用 getByTestId 選擇元素和檢查第一個測試如果按鈕禁用屬性。對于第二個,如果按鈕是否被禁用。
如果您保存文件或在終端紗線測試中再次運行,測試將通過。
3.測試事件
在編寫單元測試之前,讓我們首先看下 TestEvents.js 是什么樣子的。
import React from 'react' const TestEvents = () => { const [counter, setCounter] = React.useState(0) return ( <>{ counter }
> ) } export default TestEvents
現(xiàn)在,讓我們編寫測試。
當我們點擊按鈕時,測試計數(shù)器的增減是否正確:
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(); fireEvent.click(getByTestId('button-up')) expect(getByTestId('counter')).toHaveTextContent('1') }); it('decrements counter', () => { const { getByTestId } = render( ); fireEvent.click(getByTestId('button-down')) expect(getByTestId('counter')).toHaveTextContent('-1') });
可以看到,除了預(yù)期的文本內(nèi)容之外,這兩個測試非常相似。
第一個測試使用 fireEvent.click() 觸發(fā)一個 click 事件,檢查單擊按鈕時計數(shù)器是否增加到1。
第二個檢查當點擊按鈕時計數(shù)器是否減為-1。
fireEvent 有幾個可以用來測試事件的方法,因此您可以自由地深入文檔了解更多信息。
現(xiàn)在我們已經(jīng)知道了如何測試事件,接下來我們將在下一節(jié)中學習如何處理異步操作。
4. 測試異步操作
異步操作是需要時間才能完成的操作。它可以是HTTP請求、計時器等等。
現(xiàn)在,讓我們檢查 TestAsync.js 文件。
import React from 'react' const TestAsync = () => { const [counter, setCounter] = React.useState(0) const delayCount = () => ( setTimeout(() => { setCounter(counter + 1) }, 500) ) return ( <>{ counter }
> ) } export default TestAsync
這里,我們使用 setTimeout() 將遞增事件延遲0.5秒。
測試計數(shù)器在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(); fireEvent.click(getByTestId('button-up')) const counter = await waitForElement(() => getByText('1')) expect(counter).toHaveTextContent('1') });
要測試遞增事件,我們首先必須使用 async/await 來處理操作,因為如前所述,完成它需要時間。
接下來,我們使用一個新的助手方法 getByText()。這類似于getByTestId()。getByText()選擇文本內(nèi)容,而不是id。
現(xiàn)在,在單擊按鈕之后,我們等待 waitForElement(() => getByText('1') 來增加計數(shù)器。一旦計數(shù)器增加到1,我們現(xiàn)在可以移動到條件并檢查計數(shù)器是否等于1。
也就是說,現(xiàn)在讓我們轉(zhuǎn)向更復(fù)雜的測試用例。
你準備好了嗎?
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 ( <>{ counter }
> ) } 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 處理的基本計數(shù)器組件。
現(xiàn)在,讓我們來編寫單元測試。
測試初始狀態(tài)是否為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({component} ), store, } } afterEach(cleanup); it('checks initial state is equal to 0', () => { const { getByTestId } = renderWithRedux() expect(getByTestId('counter')).toHaveTextContent('0') }) it('increments the counter through redux', () => { const { getByTestId } = renderWithRedux( , {initialState: {count: 5} }) fireEvent.click(getByTestId('button-up')) expect(getByTestId('counter')).toHaveTextContent('6') }) it('decrements the counter through redux', () => { const { getByTestId} = renderWithRedux( , { initialState: { count: 100 }, }) fireEvent.click(getByTestId('button-down')) expect(getByTestId('counter')).toHaveTextContent('99') })
我們需要導(dǎo)入一些東西來測試 React Redux 。這里,我們創(chuàng)建了自己的助手函數(shù) renderWithRedux() 來呈現(xiàn)組件,因為它將被多次使用。
renderWithRedux() 作為參數(shù)接收要呈現(xiàn)的組件、初始狀態(tài)和存儲。如果沒有存儲,它將創(chuàng)建一個新的存儲,如果它沒有接收初始狀態(tài)或存儲,它將返回一個空對象。
接下來,我們使用render()來呈現(xiàn)組件并將存儲傳遞給提供者。
也就是說,我們現(xiàn)在可以將組件 TestRedux 傳遞給 renderWithRedux() 來測試計數(shù)器是否等于0。
測試計數(shù)器的增減是否正確:
為了測試遞增和遞減事件,我們將初始狀態(tài)作為第二個參數(shù)傳遞給renderWithRedux()?,F(xiàn)在,我們可以單擊按鈕并測試預(yù)期的結(jié)果是否符合條件。
現(xiàn)在,讓我們進入下一節(jié)并介紹 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 () } export const Counter = () => { const { counter, increment, decrement } = React.useContext(CounterContext) return ( <> { counter }
> ) } export default CounterProvider
現(xiàn)在,通過 React Context 管理計數(shù)器狀態(tài)。讓我們編寫單元測試來檢查它是否按預(yù)期運行。
測試初始狀態(tài)是否為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({component} ) } } afterEach(cleanup); it('checks if initial state is equal to 0', () => { const { getByTestId } = renderWithContext() expect(getByTestId('counter')).toHaveTextContent('0') }) it('increments the counter', () => { const { getByTestId } = renderWithContext( ) fireEvent.click(getByTestId('button-up')) expect(getByTestId('counter')).toHaveTextContent('1') }) it('decrements the counter', () => { const { getByTestId} = renderWithContext( ) fireEvent.click(getByTestId('button-down')) expect(getByTestId('counter')).toHaveTextContent('-1') })
與前面的React Redux部分一樣,這里我們使用相同的方法,創(chuàng)建一個助手函數(shù)renderWithContext()來呈現(xiàn)組件。但是這一次,它只接收作為參數(shù)的組件。為了創(chuàng)建新的上下文,我們將CounterContext傳遞給 Provider。
現(xiàn)在,我們可以測試計數(shù)器最初是否等于0。
那么,計數(shù)器的增減是否正確呢?
正如您所看到的,這里我們觸發(fā)一個 click 事件來測試計數(shù)器是否正確地增加到1并減少到-1。
也就是說,我們現(xiàn)在可以進入下一節(jié)并介紹React Router。
7. 測試 React Router
讓我們檢查一下 TestRouter.js 是什么樣子的。
TestRouter.js
import React from 'react' import { Link, Route, Switch, useParams } from 'react-router-dom' const About = () =>About page
const Home = () =>Home page
const Contact = () => { const { name } = useParams() return{name}
} const TestRouter = () => { const name = 'John Doe' return ( <>> ) } export default TestRouter
這里,將測試路由對應(yīng)的頁面信息是否正確。
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 ({component} ) } } it('should render the home page', () => { const { container, getByTestId } = renderWithRouter() 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( ) 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( ) fireEvent.click(getByTestId('contact-link')) expect(container.innerHTML).toMatch('John Doe') })
要測試React Router,我們首先必須有一個導(dǎo)航歷史記錄。因此,我們使用 createMemoryHistory() 來創(chuàng)建導(dǎo)航歷史。
接下來,我們使用助手函數(shù) renderWithRouter() 來呈現(xiàn)組件,并將歷史記錄傳遞給路由器組件。這樣,我們現(xiàn)在就可以測試在開始時加載的頁面是否是主頁。以及導(dǎo)航欄是否加載了預(yù)期的鏈接。
測試當我們點擊鏈接時,它是否用參數(shù)導(dǎo)航到其他頁面:
現(xiàn)在,要檢查導(dǎo)航是否工作,我們必須觸發(fā)導(dǎo)航鏈接上的單擊事件。
對于第一個測試,我們檢查內(nèi)容是否等于About頁面中的文本,對于第二個測試,我們測試路由參數(shù)并檢查它是否正確通過。
現(xiàn)在我們可以進入最后一節(jié),學習如何測試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 ( <> { data ?{data}:Loading...
} > ) } export default TestAxios
正如您在這里看到的,我們有一個簡單的組件,它有一個用于發(fā)出請求的按鈕。如果數(shù)據(jù)不可用,它將顯示一個加載消息。
現(xiàn)在,讓我們編寫測試。
來驗證數(shù)據(jù)是否正確獲取和顯示:
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() expect(getByTestId('loading')).toHaveTextContent('Loading...') }) it('should load and display the data', async () => { const url = '/greeting' const { getByTestId } = render( ) 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請求。
現(xiàn)在,我們可以使用axiosMock并對其應(yīng)用get()方法。最后,我們將使用Jest函數(shù)mockResolvedValueOnce()來傳遞模擬數(shù)據(jù)作為參數(shù)。
現(xiàn)在,對于第二個測試,我們可以單擊按鈕來獲取數(shù)據(jù)并使用async/await來解析它?,F(xiàn)在我們要測試三件事:
如果HTTP請求已經(jīng)正確完成
如果使用url完成了HTTP請求
如果獲取的數(shù)據(jù)符合期望。
對于第一個測試,我們只檢查加載消息在沒有數(shù)據(jù)要顯示時是否顯示。
也就是說,我們現(xiàn)在已經(jīng)完成了八個簡單的步驟來測試你的React應(yīng)用程序。
感謝各位的閱讀,以上就是“怎么使用React Testing Library和Jest完成單元測試”的內(nèi)容了,經(jīng)過本文的學習后,相信大家對怎么使用React Testing Library和Jest完成單元測試這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是創(chuàng)新互聯(lián),小編將為大家推送更多相關(guān)知識點的文章,歡迎關(guān)注!