測試驅動的 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 為組件安裝提供了兩個基本功能: shallow
和mount
。 shallow
函數僅在內存中加載根組件,而mount
加載完整的 DOM 樹。
我們將結合 Enzyme 和 Jest 來掛載 React.js 組件並在其上運行斷言。
設置我們的環境
您可以查看這個 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》。