測試驅動的 React.js 開發:使用 Enzyme 和 Jest 進行 React.js 單元測試

已發表: 2022-03-11

根據 Michael Feathers 的說法,任何沒有測試的代碼都被稱為遺留代碼。 因此,避免創建遺留代碼的最佳方法之一是使用測試驅動開發 (TDD)。

雖然有許多工具可用於 JavaScript 和 React.js 單元測試,但在這篇文章中,我們將使用 Jest 和 Enzyme 使用 TDD 創建具有基本功能的 React.js 組件。

為什麼使用 TDD 創建 React.js 組件?

TDD 為您的代碼帶來了許多好處——高測試覆蓋率的一個優點是它可以輕鬆地重構代碼,同時保持代碼的清潔和功能。

如果您之前創建過 React.js 組件,您就會意識到代碼可以增長得非常快。 它充滿了由與狀態更改和服務調用相關的語句引起的許多複雜條件。

每個缺少單元測試的組件都有難以維護的遺留代碼。 我們可以在創建生產代碼後添加單元測試。 但是,我們可能會冒著忽略一些本應經過測試的場景的風險。 通過首先創建測試,我們有更高的機會覆蓋組件中的每個邏輯場景,這將使其易於重構和維護。

我們如何對 React.js 組件進行單元測試?

我們可以使用許多策略來測試 React.js 組件:

  • 我們可以驗證當某個事件被調度時, props中的特定函數被調用了。
  • 我們還可以在給定當前組件狀態的情況下獲取render函數的結果,並將其與預定義的佈局相匹配。
  • 我們甚至可以檢查組件的子組件數量是否與預期數量相匹配。

為了使用這些策略,我們將使用兩個在 React.js 中進行測試時派上用場的工具:Jest 和 Enzyme。

使用 Jest 創建單元測試

Jest 是 Facebook 創建的一個開源測試框架,與 React.js 有很好的集成。 它包括一個用於測試執行的命令行工具,類似於 Jasmine 和 Mocha 提供的工具。 它還允許我們創建幾乎為零配置的模擬函數,並提供一組非常好的匹配器,使斷言更易於閱讀。

此外,它還提供了一個非常好的功能,稱為“快照測試”,可以幫助我們檢查和驗證組件渲染結果。 我們將使用快照測試來捕獲組件的樹並將其保存到一個文件中,我們可以使用該文件將其與渲染樹(或作為第一個參數傳遞給expect函數的任何內容)進行比較。

使用 Enzyme 掛載 React.js 組件

Enzyme 提供了一種機制來掛載和遍歷 React.js 組件樹。 這將幫助我們訪問它自己的屬性和狀態以及它的子道具,以便運行我們的斷言。

Enzyme 為組件安裝提供了兩個基本功能: shallowmountshallow函數僅在內存中加載根組件,而mount加載完整的 DOM 樹。

我們將結合 Enzyme 和 Jest 來掛載 React.js 組件並在其上運行斷言。

創建 React 組件的 TDD 步驟

設置我們的環境

您可以查看這個 repo,它具有運行此示例的基本配置。

我們使用以下版本:

 { "react": "16.0.0", "enzyme": "^2.9.1", "jest": "^21.2.1", "jest-cli": "^21.2.1", "babel-jest": "^21.2.0" }

使用 TDD 創建 React.js 組件

第一步是創建一個失敗的測試,它將嘗試使用酶的淺層函數渲染 React.js 組件。

 // MyComponent.test.js import React from 'react'; import { shallow } from 'enzyme'; import MyComponent from './MyComponent'; describe("MyComponent", () => { it("should render my component", () => { const wrapper = shallow(<MyComponent />); }); });

運行測試後,我們得到以下錯誤:

 ReferenceError: MyComponent is not defined.

然後我們創建提供基本語法的組件以使測試通過。

 // MyComponent.js import React from 'react'; export default class MyComponent extends React.Component { render() { return <div />; } }

在下一步中,我們將確保我們的組件使用 Jest 中的toMatchSnapshot函數呈現預定義的 UI 佈局。

調用此方法後,Jest 會自動創建一個名為[testFileName].snap的快照文件,並將其添加到__snapshots__文件夾中。

該文件表示我們期望從組件渲染中獲得的 UI 佈局。

但是,考慮到我們正在嘗試做TDD,我們應該先創建這個文件,然後調用toMatchSnapshot函數使測試失敗。

考慮到我們不知道 Jest 使用哪種格式來表示此佈局,這聽起來可能有點令人困惑。

您可能想先執行toMatchSnapshot函數並在快照文件中查看結果,這是一個有效的選項。 但是,如果我們真的想使用TDD,我們需要了解快照文件的結構。

快照文件包含與測試名稱匹配的佈局。 這意味著如果我們的測試有這種形式:

 desc("ComponentA" () => { it("should do something", () => { … } });

我們應該在導出部分指定這一點: Component A should do something 1

您可以在此處閱讀有關快照測試的更多信息。

因此,我們首先創建MyComponent.test.js.snap文件。

 //__snapshots__/MyComponent.test.js.snap exports[`MyComponent should render initial layout 1`] = ` Array [ <div> <input type="text" /> </div>, ] `;

然後,我們創建將檢查快照是否與組件子元素匹配的單元測試。

 // MyComponent.test.js ... it("should render initial layout", () => { // when const component = shallow(<MyComponent />); // then expect(component.getElements()).toMatchSnapshot(); }); ...

我們可以將components.getElements視為 render 方法的結果。

我們將這些元素傳遞給expect方法,以便針對快照文件運行驗證。

執行測試後,我們得到以下錯誤:

 Received value does not match stored snapshot 1. Expected: - Array [ <div> <input type="text” /> </div>, ] Actual: + Array []

Jest 告訴我們component.getElements的結果與快照不匹配。 因此,我們通過在MyComponent中添加 input 元素來通過此測試。

 // MyComponent.js import React from 'react'; export default class MyComponent extends React.Component { render() { return <div><input type="text" /></div>; } }

下一步是通過在其值更改時執行函數來向input添加功能。 我們通過在onChange中指定一個函數來做到這一點。

我們首先需要更改快照以使測試失敗。

 //__snapshots__/MyComponent.test.js.snap exports[`MyComponent should render initial layout 1`] = ` Array [ <div> <input onChange={[Function]} type="text" /> </div>, ] `;

首先修改快照的一個缺點是道具(或屬性)的順序很重要。

Jest 將在根據快照驗證它之前按字母順序對expect函數中收到的道具進行排序。 因此,我們應該按該順序指定它們。

執行測試後,我們得到以下錯誤:

 Received value does not match stored snapshot 1. Expected: - Array [ <div> onChange={[Function]} <input type="text”/> </div>, ] Actual: + Array [ <div> <input type=”text” /> </div>, ]

為了使這個測試通過,我們可以簡單地為onChange提供一個空函數。

 // MyComponent.js import React from 'react'; export default class MyComponent extends React.Component { render() { return <div><input onChange={() => {}} type="text" /></div>; } }

然後,我們確保組件的狀態在onChange事件被調度後發生變化。

為此,我們創建了一個新的單元測試,它將通過傳遞一個事件來調用輸入中的onChange函數,以模擬 UI 中的真實事件。

然後,我們驗證組件狀態是否包含一個名為input的鍵。

 // MyComponent.test.js ... it("should create an entry in component state", () => { // given const component = shallow(<MyComponent />); const form = component.find('input'); // when form.props().onChange({target: { name: 'myName', value: 'myValue' }}); // then expect(component.state('input')).toBeDefined(); });

我們現在得到以下錯誤。

 Expected value to be defined, instead received undefined

這表明組件在名為input的狀態下沒有屬性。

我們通過將此條目設置為組件的狀態來使測試通過。

 // MyComponent.js import React from 'react'; export default class MyComponent extends React.Component { render() { return <div><input onChange={(event) => {this.setState({input: ''})}} type="text" /></div>; } }

然後,我們需要確保在新狀態條目中設置了一個值。 我們將從事件中得到這個值。

所以,讓我們創建一個測試來確保狀態包含這個值。

 // MyComponent.test.js ... it("should create an entry in component state with the event value", () => { // given const component = shallow(<MyComponent />); const form = component.find('input'); // when form.props().onChange({target: { name: 'myName', value: 'myValue' }}); // then expect(component.state('input')).toEqual('myValue'); }); ~~~ Not surprisingly, we get the following error. ~~ Expected value to equal: "myValue" Received: ""

我們最終通過從事件中獲取值並將其設置為輸入值來通過此測試。

 // MyComponent.js import React from 'react'; export default class MyComponent extends React.Component { render() { return <div><input onChange={(event) => { this.setState({input: event.target.value})}} type="text" /></div>; } }

在確保所有測試都通過後,我們可以重構我們的代碼。

我們可以將onChange中傳遞的函數提取到一個名為updateState的新函數中。

 // MyComponent.js import React from 'react'; export default class MyComponent extends React.Component { updateState(event) { this.setState({ input: event.target.value }); } render() { return <div><input onChange={this.updateState.bind(this)} type="text" /></div>; } }

我們現在有一個使用 TDD 創建的簡單 React.js 組件。

概括

在這個例子中,我們嘗試使用TDD,按照每一步編寫盡可能少的代碼來失敗和通過測試。

有些步驟可能看起來沒有必要,我們可能會想跳過它們。 但是,每當我們跳過任何步驟時,我們最終都會使用不太純的 TDD 版本。

使用不太嚴格的 TDD 過程也是有效的,並且可能工作得很好。

我給你的建議是避免跳過任何步驟,如果你覺得困難也不要難過。 TDD 是一種不容易掌握的技術,但絕對值得一試。

如果您有興趣了解有關 TDD 和相關行為驅動開發 (BDD) 的更多信息,請閱讀 Toptaler Ryan Wilcox 的《你的老闆不會欣賞 TDD》。