原文連接:How to Start Testing Your React Apps Using the React Testing Library and Jestjavascript
寫測試一般都會被認做一個乏味的過程,可是這是你必須掌握的一個技能,雖然在某些時候,測試並非必要的。而後對於大多數有追求的公司而言,單元測試是必須的,開發者對於代碼的自信會大幅提升,側面來講也能提升公司對其產品的信心,也能讓用戶使用得更安心。html
在 React 世界中,咱們使用 react-testing-library 和 jest 配合使用來測試咱們的 React Apps。前端
在本文中,我將向你介紹如何使用 8 種簡單的方式來來測試你的 React App。java
本教程假定你對 React 有必定程度的瞭解,本教程只會專一於單元測試。react
接下來,在終端中運行如下命令來克隆已經集成了必要插件的項目:ios
git clone https://github.com/ibrahima92/prep-react-testing-library-guide
複製代碼
安裝依賴:git
npm install
複製代碼
或者使用 Yarn :github
yarn
複製代碼
好了,就這些,如今讓咱們瞭解一些基礎知識!npm
本文將大量使用一些關鍵內容,瞭解它們的做用能夠幫助你快速理解。redux
it 或 test
:用於描述測試自己,其包含兩個參數,第一個是該測試的描述,第二個是執行測試的函數。
expect
:表示測試須要經過的條件,它將接收到的參數與 matcher
進行比較。
matcher
:一個但願到達預期條件的函數,稱其爲匹配器。
render
:用於渲染給定組件的方法。
import React from "react";
import { render } from "@testing-library/react";
import App from "./App";
it("should take a snapshot", () => {
const { asFragment } = render(<App />);
expect(asFragment(<App />)).toMatchSnapshot();
});
複製代碼
如上所示,咱們使用 it
來描述一個測試,而後使用 render
方法來顯示 App 這個組件,同時還期待的是 asFragment(<App />)
的結果與 toMatchSnapshot()
這個 matcher
匹配(由 jest 提供的匹配器)。
順便說一句, render
方法返回了幾種咱們能夠用來測試功能的方法,咱們還使用了對象解構來獲取到某個方法。
那麼,讓咱們繼續並在下一節中進一步瞭解 React Testing Library 吧~ 。
React Testing Library 是用於測試 React 組件的很是便捷的解決方案。 它在 react-dom
和 react-dom/test-utils
之上提供了輕量且實用的 API,若是你打開 React 官網中的測試工具推薦,你會發現 Note 中寫了:
注意: 咱們推薦使用 React Testing Library,它使得針對組件編寫測試用例就像終端用戶在使用它同樣方便。
React Testing Library 是一個 DOM 測試庫,這意味着它並不會直接處理渲染的 React 組件實例,而是處理 DOM 元素以及它們在實際用戶面前的行爲。
這是一個很棒的庫,(相對)易於使用,而且鼓勵良好的測試實踐。 固然,你也能夠在沒有 Jest 的狀況下使用它。
「你的測試與軟件的使用方式越接近,就能越給你信心。」
那麼,讓咱們在下一部分中就開始使用它吧。順便說一下,你不須要安裝任何依賴了,剛纔克隆的項目自己是用 create-react-app
建立的,已經集成了編寫單元測試所須要的插件了,只需保證你已經安裝了依賴便可。
顧名思義,快照使咱們能夠保存給定組件的快照。 當你對組件進行一些更新或重構,但願獲取或比較更改時,它會頗有幫助。
如今,讓咱們對 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(<App />)).toMatchSnapshot();
});
複製代碼
要得到快照,咱們首先須要導入 render
和 cleanup
方法。 在本文中,咱們將常用這兩種方法。
你大概也猜到了, render
方法用於渲染 React 組件, cleanup
方法將做爲參數傳遞給 afterEach
,目的是在每一個測試完成後清除全部內容,以免內存泄漏。
接下來,咱們可使用 render
渲染 App 組件,並從該方法返回 asFragment
。 最後,確保 App 組件的片斷與快照匹配。
如今,要運行測試,請打開終端並導航到項目的根目錄,而後運行如下命令:
yarn test
複製代碼
若是你使用 NPM:
npm run test
複製代碼
結果,它將在 src
中建立一個新文件夾 __snapshots__
和及其目錄下新建一個 App.test.js.snap
文件,以下所示:
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
健便可對其進行更新。 而且你將在 App.test.js.snap
中擁有更新後的快照。
如今,讓咱們繼續並開始測試咱們的元素。
爲了測試咱們的 DOM 元素,咱們先大概看下 components/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
。 它將用於從測試文件中獲取到這些 dom 元素。 如今,讓咱們編寫單元測試:
TestElements.test.js
:import React from "react";
import { render, cleanup } from "@testing-library/react";
import "@testing-library/jest-dom/extend-expect";
import TestElements from "./TestElements";
afterEach(cleanup);
it("should equal to 0", () => {
const { getByTestId } = render(<TestElements />);
expect(getByTestId("counter")).toHaveTextContent(0);
});
複製代碼
如你所見,語法其實和先前的快照測試很是類似。惟一的區別是,咱們如今使用 getByTestId
進行 dom 元素的獲取,而後檢查該元素的文本內容是否爲 0
。
TestElements.test.js
(將如下代碼追加到該文件中):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
來獲取 dom 元素,第一個測試是測試 button 元素上沒有屬性 disabled
;第二個測試是測試 button 元素處於禁用狀態。
保存以後再運行測試命令,你會發現測試所有經過了!
恭喜你成功經過了本身的第一個測試!
如今,讓咱們在下一部分中學習如何測試事件。
在寫單元測試以前,咱們先來看看 components/TestEvents.js
文件是啥樣:
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;
複製代碼
如今,讓咱們爲這個組件寫單元測試。
TestEvents.test.js
:import React from "react";
import { render, cleanup, fireEvent } from "@testing-library/react";
import "@testing-library/jest-dom/extend-expect";
import TestEvents from "./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
有幾種可用於測試事件的方法,所以請隨時閱讀文檔以瞭解更多信息。
如今咱們知道了如何測試事件,讓咱們繼續學習下一節如何處理異步操做。
異步操做須要花費一些時間才能完成。它能夠是 HTTP 請求,計時器等。
一樣地,讓咱們檢查一下 components/TestAsync.js
文件。
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()
模擬異步。
TestAsync.test.js
:import React from "react";
import {
render,
cleanup,
fireEvent,
waitForElement,
} from "@testing-library/react";
import "@testing-library/jest-dom/extend-expect";
import TestAsync from "./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()
類似,只是如今咱們經過 dom 元素的文本內容去獲取該元素而已,而不是以前使用的 test-id
。
如今,單擊按鈕後,咱們等待使用 waitForElement(() => getByText('1'))
遞增計數器。 計數器增長到 1
後,咱們如今能夠移至條件並檢查計數器是否有效等於 1
。
是否是理解起來很簡單?話雖如此,讓咱們如今轉到更復雜的測試用例。
你準備好了嗎?
若是您不熟悉 React Redux,本文可能會爲你提供些許幫助。先讓咱們看一下 components/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 處理的基本計數器組件。
如今,讓咱們編寫單元測試。
TestRedux.test.js
:import React from "react";
import { createStore } from "redux";
import { Provider } from "react-redux";
import { render, cleanup, fireEvent } from "@testing-library/react";
import "@testing-library/jest-dom/extend-expect";
import { initialState, reducer } from "../store/reducer";
import TestRedux from "./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");
});
複製代碼
咱們須要導入一些內容來測試 React Redux。在這裏,咱們建立了本身的輔助函數 renderWithRedux()
來渲染組件,由於它將屢次被使用到。
renderWithRedux()
接收要渲染的組件, initialState
和 store
做爲參數。若是沒有 store
,它將建立一個新 store
,若是沒有收到 initialState
或 store
,則將返回一個空對象。
接下來,咱們使用 render()
渲染組件並將 store
傳遞給 Provider
。
意味着,咱們如今能夠將組件 TestRedux
傳遞給 renderWithRedux()
來測試計數器是否等於 0
。
TestRedux.test.js
(將如下代碼追加到該文件中):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");
});
複製代碼
爲了測試遞增和遞減事件,咱們將initialState
做爲第二個參數傳遞給 renderWithRedux()
。 如今,咱們能夠單擊按鈕並測試預期結果是否符合條件。
如今,讓咱們進入下一部分並介紹 React Context。
再接下來是 React Router 和 Axios,你還會看下去嗎?
若是您不熟悉 React Context,請先閱讀本文。另外,讓咱們看下 components/TextContext.js
文件。
TextContext.js
:import React, { createContext, useContext, useState } from "react";
export const CounterContext = createContext();
const CounterProvider = () => {
const [counter, setCounter] = 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 } = 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 進行管理,讓咱們編寫單元測試以檢查其行爲是否符合預期。
TestContext.test.js
:import React from "react";
import { render, cleanup, fireEvent } from "@testing-library/react";
import "@testing-library/jest-dom/extend-expect";
import CounterProvider, { CounterContext, Counter } from "./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");
});
複製代碼
與上一節關於 React Redux 的部分同樣,這裏咱們經過建立一個輔助函數 renderWithContext()
來渲染組件。可是此次,它僅接收組件做爲參數。 爲了建立一個新的上下文,咱們將 CounterContext
傳遞給 Provider。
如今,咱們就能夠測試計數器初始狀態是否等於 0
。
TestContext.test.js
(將如下代碼追加到該文件中):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");
});
複製代碼
如你所見,這裏咱們觸發一個 click 事件,測試計數器是否正確地增長到 1
或減小到 -1
。
咱們如今能夠進入下一節並介紹 React Router。
若是您想深刻研究 React Router,這篇文章可能會對你有所幫助。如今,讓咱們先 components/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 { Router } from "react-router-dom";
import { render, fireEvent } from "@testing-library/react";
import "@testing-library/jest-dom/extend-expect";
import { createMemoryHistory } from "history";
import TestRouter from "./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);
});
複製代碼
要測試 React Router,咱們首先必須有一個導航 history。所以,咱們使用 createMemoryHistory()
來建立導航 history 。
接下來,咱們使用輔助函數 renderWithRouter()
渲染組件並將 history
傳遞給 Router
組件。 這樣,咱們如今能夠測試在開始時加載的頁面是不是主頁,並在導航欄中渲染預期中的 Link
組件。
TestRouter.test.js
(將如下代碼追加到該文件中):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");
});
複製代碼
要檢查導航是否有效,咱們必須在導航連接上觸發 click 事件。
對於第一個測試,咱們檢查內容是否與「About Page」中的文本相等,對於第二個測試,咱們測試路由參數並檢查其是否正確傳遞。
如今,咱們能夠轉到最後一節,學習如何測試 Axios 請求。
咱們快完成了!加油啊!
像往常同樣,讓咱們首先看一下 components/TextAxios.js
文件內容。
TestAxios.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;
複製代碼
如你所見,咱們有一個簡單的組件,該組件帶有一個用於發出請求的按鈕。而且若是數據不可用,它將顯示一條加載中的消息(Loading...)。
如今,讓咱們編寫測試。
TestAxios.test.js
:import React from "react";
import { render, waitForElement, fireEvent } from "@testing-library/react";
import "@testing-library/jest-dom/extend-expect";
import axiosMock from "axios";
import TestAxios from "./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
來處理異步請求。如今咱們必須保證如下 3
個測試經過:
對於第一個測試,咱們只檢查沒有數據要顯示時是否顯示加載消息(loading...)。
到如今爲止,咱們如今已經使用了 8
個簡單步驟完成了 React Apps 的測試了。
如今的你是否已經感受入門了呢?請查閱更多文檔信息進階吧,如下是一些推薦閱讀:
React Testing Library docs
React Testing Library Cheatsheet
Jest DOM matchers cheatsheet
Jest Docs
Testing with react-testing-library and Jest
前端自動化測試 jest 教程 1-配置安裝
前端自動化測試 jest 教程 2-匹配器 matchers
前端自動化測試 jest 教程 3-命令行工具
前端自動化測試 jest 教程 4-異步代碼測試
前端自動化測試 jest 教程 5-鉤子函數
前端自動化測試 jest 教程 6-mock 函數
前端自動化測試 jest 教程 7-定時器測試
前端自動化測試 jest 教程 8-snapshot 快照測試
React Testing Library 是用於測試 React 組件的出色插件包。它使咱們可以訪問 jest-dom 的 matcher,咱們可使用它們來更有效地並經過良好實踐來測試咱們的組件,但願本文對你有所幫助。
感謝您閱讀!
這是個人 github/blog,若對你有所幫助,賞個小小的 star 🌟 咯~