Unit Test Counter

Frank
Frank
Feb 22 · 17 min read

深入淺出學習 Jest 搭配 Enzyme 進行單元測試

建立專案

此篇文章將手牽手帶領大家使用 JestEnzyme 進行 react unit test ,腳手架選擇官方的 create-react-app ,首先來建立一個專案吧!

➔ npx create-react-app unit-test-counter
➔ cd my-app
➔ npm start
npm start 看到的畫面

修改腳手架的檔案,讓專案更加乾淨

啟動後可以在遊覽器看到經典的起始畫面,但這不是我們要的,我們要先來實作計數器,為了呈現比較乾靜、簡潔的環境,可以先把一些預設的檔案修改、刪除。

■ src
|→ App.css
|→ App.js
|→ App.test.js
|→ index.css
|→ index.js
|→ logo.svg
|→ serviceWorker.js
  1. App.css [可刪除]。
  2. App.js 主要的入口元件,只要負責引入計數器元件即可,[需要修改]。
  3. App.test.js 預設的測試檔案,本篇主要針對計數器元件測試,[可刪除]。
  4. index.css 沒有要使用到,[可刪除]。
  5. index.js 暫時沒有要使用到server worker這個功能,可以不用引入,[需要修改]。
  6. logo.svg 預設畫面要呈現的圖片檔,計數器沒有要使用到,[可刪除]。
  7. serviceWorker.js 暫時沒有要使用,[可刪除]。

所以需要修改的只有兩個地方,index.js 將變成如下:

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
ReactDOM.render(<App />, document.getElementById('root'));

而另一個入口元件 App.js,只負責一件事情,引入計數器元件!

import React, { Component } from 'react';
import Counter from './Counter'
class App extends Component {
render() {
return (
<div className="app">
<Counter />
</div>
);
}
}
export default App;

建立計數器元件

此時會有一個小小的錯誤,顯示找不到Counter元件,這是因為我們還沒有建立呀~~馬上來創建 Counter.js 以及 counter.css ,這個計數器功能非常簡單,畫面上可以看到目前計數的數值,下方會有兩個按鈕,一個點擊會把計數加一,另一個點擊則計數減一。

import React, { Component } from 'react';
import './counter.css'
class Counter extends Component {
constructor(props) {
super(props);

this.state = {
counter: 0
}
}

handleIncreaseOne = () => {
this.setState({ counter: this.state.counter + 1 })
}
handleDecreaseOne = () => {
this.setState({ counter: this.state.counter - 1 })
}
render() {
const { counter } = this.state
return (
<div className="counter-page">
<h1 className="count">counter: { counter }</h1>
<button type="button" className="btn-inc" onClick={this.handleIncreaseOne}>+ 1</button>
<button type="button" className="btn-dec" onClick={this.handleDecreaseOne}>- 1</button>
</div>

);
}
}
export default Counter;

樣式部分我只有簡單的把兩個按鈕間距加大,你可以根據自己的喜好修改.

.counter-page button {
margin: 0 .5rem;
}
計數器元件的遊覽器畫面

到目前為止照著上面的順序往下操作,就可以看到上方的瀏覽器畫面,兩個按鈕也可以正常運作,那麼接下來才是重頭戲,什麼前面還沒有開始?前面只是暖身,現在才要開始!

大家可以先打開 package.json 這個檔案,在預設的 scripts 當中其實已經有包含測試指令了,前面我們刪除的檔案 App.test.js ,就是在測試 App.js 這個元件,但是我們主要是要實作測試計數器元件,所以才刪除它。

"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},

附帶一提,使用官方的腳手架本身就已經安裝好 Jest 了,所以我們只要安裝 Enzyme 就可以讓兩者好好的搭配,如果你想知道為什麼要用 Jest 搭配 Enzyme, 這裡有個任意門可以補充一下知識。


安裝 Enzyme 相關套件

Enzyme 相關套件,我們會使用到三個:

  1. enzyme :也就是套件本身,廢話。
  2. enzyme-adapter-react-16 :負責和 react 版本16以上進行橋接溝通用, enzyme官方文件說請記得安裝。
  3. enzyme-to-json:轉換快照格式讓Jest相容。

關於第二點官方提到要建置 enzyme 的環境,可以把設定檔案進行抽離,什麼是設定檔案呢?就是以下的這三行。如果不想把設定檔案抽離,那麼每一支測試檔當中都要寫這三行,就會變得非常不聰明~註解中我有提到這個設定檔在 create-react-app 腳手架中,本身有預設檔名和路徑了,所以我們只要跟隨規則,在 src 下創建 setupTest.js 檔案就可以囉~

/*
* setupTest file
* create-react-app default config rootDir: src/setupTest.js
*/
import Enzyme from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
// Setup enzyme's react adapter
Enzyme.configure({ adapter: new Adapter() });

最後在進行 Counter.test.js 前,有一些簡單的先備知識先來補充一下,這樣大家比較好上手。

Enzyme渲染元件的三種模式

  1. shallow rendering:又可以稱為淺渲染,將一個組件渲染成虛擬DOM對象,但不會渲染其內部的子組件,也不是真正完整的React Render,所以無法與子組件互動,優點是可提高測試效能。
  2. full DOM rendering:又稱完全渲染,它將組件渲染加載成一個真實的DOM節點,用來測試DOM API的交互和組件的生命週期,用到了jsdom來模擬瀏覽器環境。
  3. static rendered markup:靜態渲染,顧名思義它將React組件渲染成靜態的HTML字符串,接著使用Cheerio函式庫解析這段字符串,並返回一個Cheerio的實例對象,可以用來分析組件的html結構。

describe 和 it 語句常常看到,到底是什麼?

另一方面在撰寫測試文件時,經常會看到 describeit 這兩個名詞,這裡也來解說一下,it 等同於 test 語句,表示定義規範(Define a single spec),規範中必須包含一個或多個期望(expectations)來測試程式碼的狀態,就像這樣子。

it 語句表示圖

expect() 這個方法是 Jest 提供的API,參數就是想要測試的東西,可以是變數、字串、數字、布林。

toBeTruthy() 也是Jest 提供,語意為:成為布林值 TURE

兩者結合在一起就變成,期望你給我的測試輸入,數值將會是 TURE,如果符合你就可以在 command-line 看到綠色勾勾,不符合就會是紅色的叉叉,並顯示接收到的數值和期望值.來看看測試結果圖。

測試結果圖

因為我們在expect() 中輸入測試的內容是 true,所以期望的 toBeTruthy() 就會是符合的!可以看到測試結果中,在測試規格1前面出現了綠色的勾勾,這就代表測試成功。

測試結果圖中可以看到 ”測試一組規範” 的文字,其實就是 describe 這個語句,我們定義的名稱。describe 指的是一系列的規範(又可稱 a suite),似懂非懂嗎!?它其實就像這樣子。表示圖中一共有三組規範(規格),在外層有一個describe 把他們包覆起來,這就是 describe 的功能。

describe 語句和三個測試規範 表示圖

當測試都成功,就會看到三個綠色的勾勾!

測試結果圖

稍微改一下測試規範後,如果測試失敗,將會看到…

Jest 非常貼心的提示顯示出來,從接受到的數值可以看到是 false,而我們預期他會是 true,顯而知,這一組規範測試失敗!


重點整理

來稍微統整一下上述觀念,測試文件中,使用it語句來寫測試規範,當有很多組測試規範就可以使用describe 語句來包覆,讓同樣類型的測試歸屬在一區,好比我們要測試 <Counter /> 元件,就可以用這個方式,把所有的相關的測試規範寫在describe 中。更白話的說就是describe 區域內會有很多 it (至少要有一個)。


使用 Enzyme shallow rendering

渲染輸出元件的實例,使用這個方式得到的 ShallowWrapper ,如果透過打印其實可以看到整個元件的節點圖,這一步驟算是一個起手式,如果要測試某個元件的時候,就可以透過前面介紹的三種 Enzyme 渲染元件 API 之一,進行渲染,這個行為等同拿到整個元件,後續要進行遍歷或觀測狀態都是非常方便的。

const wrapper = shallow(<Counter />)
console.log wrapper.debug()

檢查渲染的結果是不是符合預期

此範例一共檢查三個節點,分別為 count 和另外兩個按鈕

1. 包含一個樣式名為count的節點

檢查這個項目是因為渲染當中包含了一個狀態 counter ,所以列為檢查的重點之一,除了 render 要對,後續也要檢查狀態。

<h1 className=”count”>counter: { counter }</h1>

it('1. 包含一個樣式名為count的節點', () => {
expect(wrapper.find('.count').exists()).toBeTruthy()
})

2. 包含一個樣式名為btn-inc的節點

此按鈕是觸發自訂函式的關鍵,也需要檢查是否 render 正確

<button type=”button” className=”btn-inc” onClick={this.handleIncreaseOne}>+ 1</button>

it('2. 包含一個樣式名為btn-inc的節點', () => {
expect(wrapper.find('.btn-inc').exists()).toBeTruthy()
})

3. 包含一個樣式名為btn-dec的節點

同 btn-inc 按鈕

<button type=”button” className=”btn-dec” onClick={this.handleDecreaseOne}>- 1</button>

it('3. 包含一個樣式名為btn-dec的節點', () => {
expect(wrapper.find('.btn-dec').exists()).toBeTruthy()
})

expect()方法中可以看到,我們在 wrapper 後串連 find() 方法,此方法是 enzyme 特有的語法糖,與當年流行的 jquery 87%像呢!只要在 find() 中寫入節點的 class 選擇器id 選擇器,就可以進行遍歷。後方再串連上 exists() 方法來判斷節點是否存在於 wrapper 底下,假若存在回傳 true,否則回傳 false


檢查元件的狀態

計數器中最重要的狀態就是 counter ,是另一個重點檢查對象

4. counter 初始值為零

這項檢查主要是要確保 counter 的初始值是正確的,後續我們才可以透過模擬點擊按鈕的方式,讓 counter 數值改變

it('4. counter 初始值為零', () => {
expect(wrapper.state('counter')).toBe(0)
})

又看到新的使用方法了! state() 輸入想要查詢的狀態名稱,就會回傳數值了。


模擬點擊事件

這是一個 enzyme 中一個非常好用的功能,提供了模擬的行爲,因為在元件中常常會撰寫互動的行為,執行相對應的事件,有了 simulate() 就可以讓我們事半功倍

5. 點擊+1按鈕

計數器的第一個按鈕,點擊按鈕,計數加一,透過模擬點擊,檢查狀態是否正確

it('5. 點擊+1按鈕', () => {
wrapper.find('.btn-inc').simulate('click')
expect(wrapper.state('counter')).toBe(1)
})

6. 點擊-1按鈕

計數器的第二個按鈕,點擊按鈕,計數減ㄧ,這裡要特別注意,前面我們已經模擬點擊一次了,這時 counter: 1 ,此時我們再點擊另一個按鈕,就會把當前的更新的狀態進行運算,counter: 0 ,這會是我們預期的結果

it('6. 點擊-1按鈕', () => {
wrapper.find('.btn-dec').simulate('click')
expect(wrapper.state('counter')).toBe(0)
})

snapshot 節點快照

為了讓 ShallowWrapper 可以轉換成為 jest 相容的快照格式,前面安裝了enzyme-to-json 這個套件就要派上場了~快照簡單的說很像我們去照X光,它可以把一個元件的渲染的結果拍下來,這當中有什麼節點都可以看得很清楚,但在實際開發測試上實不實用就看團隊的討論和需求了。

7. 輸出snapshot

記得先引入 enzyme-to-json ,接著把 ShallowWrapper 放入 轉換格式方法,最後呼叫 jest 進行快照

it('7. 輸出snapshot', () => {
expect(toJson(wrapper)).toMatchSnapshot()
})

拍照成功後會自動在 src 底下建立 __snapshots__ 資料夾,並把結果圖放在裡面

snapshot 後檔案結構圖

而大家最關心的快照到底長得什麼樣子,其實就像這樣!

counter 元件的 snapshot

以上我們就完成了七個的不同的規範的計數器元件測試,為了讓大家好閱讀,附上整個測試檔案的程式碼,每一小段上方都有講解~

import React from 'react';
import { shallow } from 'enzyme';
import toJson from 'enzyme-to-json';
import Counter from './Counter';
describe('測試<Counter />', () => {
const wrapper = shallow(<Counter />)
it('1. 包含一個樣式名為count的節點', () => {
expect(wrapper.find('.count').exists()).toBeTruthy()
})
it('2. 包含一個樣式名為btn-inc的節點', () => {
expect(wrapper.find('.btn-inc').exists()).toBeTruthy()
})
it('3. 包含一個樣式名為btn-dec的節點', () => {
expect(wrapper.find('.btn-dec').exists()).toBeTruthy()
})
it('4. counter 初始值為零', () => {
expect(wrapper.state('counter')).toBe(0)
})
it('5. 點擊+1按鈕', () => {
wrapper.find('.btn-inc').simulate('click')
expect(wrapper.state('counter')).toBe(1)
})
it('6. 點擊-1按鈕', () => {
wrapper.find('.btn-dec').simulate('click')
expect(wrapper.state('counter')).toBe(0)
})
it('7. 輸出snapshot', () => {
expect(toJson(wrapper)).toMatchSnapshot()
})
})
計數器測試結果 command-line

計數器單元測試總結

  1. 透過 jest 提供的大平台,有方便的 expect()toBe() 等等可以使用,又有整合好的 commad-line 非常方便。
  2. Enzymejquery 語法糖的遍歷節點API,真的非常好上手!也有模擬事件的方式,讓測試react元件更加的簡單。
  3. 看到一個元件,我們到底要測試什麼?套一句 Ryan Walsh (enzyme)大神所說的話,Anything that is not static ,也就是說只要是會改變的地方就要測試。
    如果有互動事件,就要測試!
    如果有輸出狀態,就要測試!
    如果有會改變的 className ,就要測試!

BUT,越多的測試所需要的時間也就越多,不用盲目追求100%測試覆蓋率,衡量專案的類型,並確保最重要的商業邏輯區塊是正確的!


範例 repo: LINK

如果覺得文章還不錯,對你有一點點幫助,請不要吝嗇給我 clap ,感謝你 :D

持續分享好文章 生活、知識、技術 , etc.

Frank

Written by

Frank

喜歡學習新事物,動手自己做做看,享受生活的美學.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade