如何使用React Testing Library和Jest測試React應用

原文連接:How to Start Testing Your React Apps Using the React Testing Library and Jestjavascript

寫測試一般都會被認做一個乏味的過程,可是這是你必須掌握的一個技能,雖然在某些時候,測試並非必要的。而後對於大多數有追求的公司而言,單元測試是必須的,開發者對於代碼的自信會大幅提升,側面來講也能提升公司對其產品的信心,也能讓用戶使用得更安心。html

在 React 世界中,咱們使用 react-testing-libraryjest 配合使用來測試咱們的 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 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  建立的,已經集成了編寫單元測試所須要的插件了,只需保證你已經安裝了依賴便可。

8 個示例

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(<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  中擁有更新後的快照。

如今,讓咱們繼續並開始測試咱們的元素。

2.測試 DOM 元素

爲了測試咱們的 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 元素。 如今,讓咱們編寫單元測試:

測試計數器(counter)是否等於 0

  • 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 。

測試 button 按鈕是禁用仍是啓用

  • 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 元素處於禁用狀態。

保存以後再運行測試命令,你會發現測試所有經過了!

恭喜你成功經過了本身的第一個測試!

如今,讓咱們在下一部分中學習如何測試事件。

3.測試事件

在寫單元測試以前,咱們先來看看 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  有幾種可用於測試事件的方法,所以請隨時閱讀文檔以瞭解更多信息。

如今咱們知道了如何測試事件,讓咱們繼續學習下一節如何處理異步操做。

4.測試異步操做

異步操做須要花費一些時間才能完成。它能夠是 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()  模擬異步。

測試計數器是否在 0.5s 後遞增

  • 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 。

是否是理解起來很簡單?話雖如此,讓咱們如今轉到更復雜的測試用例。

你準備好了嗎?

5.測試 React Redux

若是您不熟悉 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 處理的基本計數器組件。

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

測試初始狀態是否等於 0

  • 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,你還會看下去嗎?

6.測試 React Context

若是您不熟悉 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 進行管理,讓咱們編寫單元測試以檢查其行爲是否符合預期。

測試初始狀態是否等於 0

  • 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。

7.測試 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 請求。

咱們快完成了!加油啊!

8.測試 HTTP Request

像往常同樣,讓咱們首先看一下 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  個測試經過:

  • HTTP 請求執行了正確的次數?
  • HTTP 請求是否已經過 url 完成?
  • 獲取的數據是否符合指望?

對於第一個測試,咱們只檢查沒有數據要顯示時是否顯示加載消息(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 🌟 咯~

相關文章
相關標籤/搜索