使用 React Testing Library 和 Jest 完成單元測試

java06051515發表於2020-03-24

引言

在2020的今天,構建一個 web 應用對於我們來說,並非什麼難事。因為有很多足夠多優秀的的前端框架(比如  ReactVue 和  Angular);以及一些易用且強大的UI庫(比如  Ant Design)為我們保駕護航,極大地縮短了應用構建的週期。

但是,網際網路時代也急劇地改變了許多軟體設計,開發和釋出的方式。開發者面臨的問題是,需求越來越多,應用越來越複雜,時不時會有一種失控的的感覺,並在心中大喊一句:“我太南了!”。嚴重的時候甚至會出現我改了一行程式碼,卻不清楚其影響範圍情況。這種時候,就需要測試的方式,來保障我們應用的質量和穩定性了。

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

需要什麼樣的測試

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

  • 級別1
    • 使用測試覆蓋率工具。
    • 使用持續整合。
    • 測試分級為小型、中型、大型。
    • 建立冒煙測試集合(主流程測試用例)。
    • 標記哪些測試是非確定性的測試(測試結果不唯一)。


  • 級別2
    • 如果有測試執行結果為紅色(失敗❌)就不會發布。
    • 每次程式碼提交之前都要求透過冒煙測試。(自測,簡單走下主流程)
    • 各種型別的整體程式碼覆蓋率要大於50%。
    • 小型測試的覆蓋率要大於10%。


  • 級別3
    • 所有重要的程式碼變更都要經過測試。
    • 小型測試的覆蓋率大於50%。
    • 新增重要功能都要透過整合測試的驗證。


  • 級別4
    • 在提交任何新程式碼之前都會自動執行冒煙測試。
    • 冒煙測試必須在30分鐘內執行完畢。
    • 沒有不確定性的測試。
    • 總體測試覆蓋率應該不小於40%。
    • 小型測試的程式碼覆蓋率應該不小於25%。
    • 所有重要的功能都應該被整合測試驗證到。


  • 級別5
    • 對每一個重要的缺陷修復都要增加一個測試用例與之對應。
    • 積極使用可用的程式碼分析工具。
    • 總體測試覆蓋率不低於60%。
    • 小型測試程式碼覆蓋率應該不小於40%。


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

而對於開發者來說,重要的是進行了測試的動作。本篇文章主要圍繞著 React元件單元測試展開的,其目的是為了讓開發人員可以站在使用者的角度考慮問題。透過測試的手段,確保元件的每一個功能都可以正常的執行,關注質量,而不是讓使用者來幫你測試。

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

技術棧選擇

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

下面讓我們看一個簡單的計數器的例子,以及兩個相應的測試:第一個是使用  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 (
      <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.js
import 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.js
import 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.js
export 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.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 (
    <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 通常不需要指定選項。 官方文件在這裡,如果要指定的話,如下值是對官方文件的簡單摘錄:

  • container:React Testing庫將建立一個div並將該div附加到文件中。而透過這個引數,可以自定義容器。
  • baseElement:
    如果指定了容器,則此值預設為該值,否則此值預設為document.documentElement。這將用作查詢的基本元素,以及在使用debug()時列印的內容。
  • hydrate:用於服務端渲染,使用  ReactDOM.hydrate 載入你的元件。
  • wrapper:傳遞一個元件作為包裹層,將我們要測試的元件渲染在其中。這通常用於建立可以重用的自定義 render 函式,以便提供常用資料。
  • queries:查詢繫結。除非合併,否則將覆蓋DOM測試庫中的預設設定。

基本上,這個函式所做的就是使用ReactDOM呈現元件。在直接附加到document.body的新建立的div中呈現(或為伺服器端呈現提供水合物)。因此,可以從DOM測試庫和其他一些有用的方法(如debug、rerender或unmount)獲得大量查詢。

文件: testing-library.com/doc

但你可能會想,這些問題是什麼呢?有些實用程式允許您像使用者那樣查詢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);
// OR
fireEvent.click(decrementButton);

斷言(Assert)

接下來是最後一部分。觸發事件通常會觸發應用程式中的一些更改,因此我們必須執行一些斷言來確保這些更改發生。在我們的測試中,這樣做的一個好方法是確保呈現給使用者的計數已經更改。因此,我們只需斷言textContent屬性的計數器是遞增或遞減:

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

恭喜你,到這裡你已經將我們的示例拆解成功。 ?

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

8個典型的例子

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

安裝依賴的同時可以簡單看下我們的專案。 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, 
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官方文件

結語

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

作者:張博軒

現在註冊滴滴雲,有機會可得30元無門檻滴滴出行券

新購雲服務1月5折 3月4.5折 6月低至4折

滴滴雲使者招募,推薦最高返傭50%


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31559758/viewspace-2682344/,如需轉載,請註明出處,否則將追究法律責任。

相關文章