原文地址: medium.com/javascript-…
譯文地址:github.com/xiao-T/note…
本文版權歸原做者全部,翻譯僅用於學習。javascript
單元測試是一門很是偉大的學科,它能夠減小40%-80%的 bug。同時,還有如下幾個重要的好處:html
console.log()
和直接點擊UI驗證改變是否正確。做爲一個單元測試新手可能須要在 TDD 流程上花費額外的15% - 30% 的時間瞭解如何測試各類組件,可是,TDD 經驗豐富的開發者會節省具體實現的時間。有些狀況單元測試相對比較容易。舉例來講,單元測試對純函數更加有效:一個函數,也就意味着一樣的輸入總會獲得一樣的輸出,不會有反作用。java
可是,UI 組件並不屬於這一類,這使得 TDD 更加艱難,須要先編寫測試。node
對於我列出好處中有一些先編寫測試用例是必要的,好比:在開發應用過程當中,改善結構、更好的開發體驗和更快的反饋。做爲一個開發者要練習使用 TDD。不少開發者喜歡在編寫測試以前編寫業務,若是,你不先編寫測試,你就會失去不少單元測試帶來的好處。react
儘管如此,先編寫測試仍是值得實踐的。TDD 和單元測試可讓你編寫 UI 組件更加簡單、更容易維護和更容易組合複用組件。git
我在測試領域最新的一個發明就是:實現了單元測試框架 RITEway,它是對 Tape 的簡單封裝,讓你編寫測試更加簡單、更容易維護。github
無論你用什麼測試框架,接下來的提示均可以幫助你編寫更好、更易測試、更具可讀性和更具可組合性的 UI 組件:shell
一個函數組件,也就意味着一樣的 props,中會渲染一樣的 UI,也不會有反作用。好比:數據庫
import React from 'react';
const Hello = ({ userName }) => (
<div className="greeting">Hello, {userName}!</div>
);
export default Hello;
複製代碼
這類組件一般很是容易測試。你須要一方式選擇定位組件(在這個示例中,咱們經過類名 greeting
來選擇組件),而後,你須要知道組件輸出什麼。爲純函數組件編寫測試用例,我使用 RITEway
中的 render-component
。npm
首先,須要安裝 RITEway:
npm install --save-dev riteway
複製代碼
RITEway 內部使用 react-dom/server
renderToStaticMarkup()
而後把輸出的內容包裝成 Cheerio 對象以便選擇。若是,你不使用 RITEway,你也建立屬於本身的功能函數把 React 組件渲染成靜態標籤,而後用 Cheerio 來操做。
一旦,你獲得 Cheerio 對象,你就能夠像下面這樣編寫測試:
import { describe } from 'riteway';
import render from 'riteway/render-component';
import React from 'react';
import Hello from '../hello';
describe('Hello component', async assert => {
const userName = 'Spiderman';
const $ = render(<Hello userName={userName} />); assert({ given: 'a username', should: 'Render a greeting to the correct username.', actual: $('.greeting') .html() .trim(), expected: `Hello, ${userName}!` }); }); 複製代碼
可是,這並沒什麼神奇的。若是,你須要測試 stateful 組件或者有反作用的組件呢?這纔是 TDD 對 React 組件真正神奇的地方,由於,這個問題的答案同另一個很是重要的問題答案相同:「如何讓組件更容易維護和 debug?」。
回答是:從組件中隔離 state 和反作用。你能夠把 state 和反作用封裝到一個容器組件,而後,把 state 作爲純函數組件的 props 向下傳遞。
可是,Hooks API 不就是爲了讓組件層級更加扁平,避免更深層的嵌套嗎?不徹底是。把組件分紅三類仍舊是一個很好的注意,讓彼此相互隔離:
根據我我的的經驗,若是,你將顯示/UI 與程序邏輯和反作用分離開,會提高你的開發體驗。這種規則在我使用過的每種語言或者每一個框架中,包括 React Hooks,都適用。
咱們來建立一個 Counter 組件來演示 stateful 組件。首先,咱們須要建立 UI 組件。它應該包括這些內容:「Clicks: 13」 來表示按鈕被點擊了多少次。按鈕的值是「Click」。
爲這個顯示組件編寫單元測試很是簡單。咱們只需測試按鈕是否被渲染(咱們不關心按鈕的值是什麼 — 根據用戶的設置,不一樣的語言會有不一樣的顯示)。咱們還想知道是否顯示了正確的點擊數。咱們須要編寫兩個測試:一個測試按鈕是否顯示,另一個驗證點擊次數是否顯示正確。
使用 TDD 時,我習慣使用兩種不一樣的斷言來確保組件能夠正確顯示相關的 props。若是,只編寫一個測試有可能正好對應組件中的 hard-code。爲了不這種狀況,你能夠用兩個不一樣的值來編寫不一樣測試用例。
這個示例中,咱們建立了一個名叫 <ClickCounter>
的組件,組件會有一個名爲 clicks
的屬性表明點擊次數。爲了使用它,只需爲組件設置一個 clicks
屬性,來表示須要顯示的數字便可。
咱們來看一下單元測試是如何保證組件渲染的。咱們須要建立新文件:click-counter/click-counter-component.test.js
:
import { describe } from 'riteway';
import render from 'riteway/render-component';
import React from 'react';
import ClickCounter from '../click-counter/click-counter-component';
describe('ClickCounter component', async assert => {
const createCounter = clickCount =>
render(<ClickCounter clicks={ clickCount } />) ; { const count = 3; const $ = createCounter(count); assert({ given: 'a click count', should: 'render the correct number of clicks.', actual: parseInt($('.clicks-count').html().trim(), 10), expected: count }); } { const count = 5; const $ = createCounter(count); assert({ given: 'a click count', should: 'render the correct number of clicks.', actual: parseInt($('.clicks-count').html().trim(), 10), expected: count }); } }); 複製代碼
爲了更加簡單的編寫測試用例,我喜歡建立小的工廠函數。這個示例中,createCounter
須要一個數字參數,而後,返回一個渲染後的組件:
const createCounter = clickCount =>
render(<ClickCounter clicks={ clickCount } />) ; 複製代碼
有了測試用例,是時候實現 ClickCounter
組件了。我把組件和測試文件放在了同一目錄下,並命名爲 click-counter-component.js
。首先,咱們先編寫組件的框架,而後,你會看到測試用例報錯了:
import React, { Fragment } from 'react';
export default () =>
<Fragment> </Fragment>
;
複製代碼
若是,咱們保存而後運行測試用例,你會看到報錯TypeError
,它觸發了 Node 的 UnhandledPromiseRejectionWarning
。最終,Node 將不會使用煩人的警告 DeprecationWarning
,而是拋出一個 UnhandledPromiseRejectionError
錯誤。咱們之因此遇到這個 TypeError
,是由於咱們選擇器返回了 null
,而後,咱們嘗試調用 null
的 trim()
方法。咱們能夠經過渲染指望的結構來修復這個錯誤:
import React, { Fragment } from 'react';
export default () =>
<Fragment> <span className="clicks-count">3</span> </Fragment>
;
複製代碼
很好。如今,咱們應該會有一個測試經過,一個測試失敗:
# ClickCounter component
ok 2 Given a click count: should render the correct number of clicks.
not ok 3 Given a click count: should render the correct number of clicks.
---
operator: deepEqual
expected: 5
actual: 3
at: assert (/home/eric/dev/react-pure-component-starter/node_modules/riteway/source/riteway.js:15:10)
...
複製代碼
爲了修復它,咱們須要把 count 設置爲組件的 prop,而後用真實的值來渲染:
import React, { Fragment } from 'react';
export default ({ clicks }) =>
<Fragment> <span className="clicks-count">{ clicks }</span> </Fragment>
;
複製代碼
如今,咱們全部的測試都經過了:
TAP version 13
# Hello component
ok 1 Given a username: should Render a greeting to the correct username.
# ClickCounter component
ok 2 Given a click count: should render the correct number of clicks.
ok 3 Given a click count: should render the correct number of clicks.
1..3
# tests 3
# pass 3
# ok
複製代碼
是時候測試點擊按鈕了。首先,添加測試用例,很顯然會失敗:
{
const $ = createCounter(0);
assert({
given: 'expected props',
should: 'render the click button.',
actual: $('.click-button').length,
expected: 1
});
}
複製代碼
這是測試失敗後的提示:
not ok 4 Given expected props: should render the click button
---
operator: deepEqual
expected: 1
actual: 0
...
複製代碼
如今,咱們來實現點擊按鈕:
export default ({ clicks }) =>
<Fragment> <span className="clicks-count">{ clicks }</span> <button className="click-button">Click</button> </Fragment>
;
複製代碼
測試經過了:
TAP version 13
# Hello component
ok 1 Given a username: should Render a greeting to the correct username.
# ClickCounter component
ok 2 Given a click count: should render the correct number of clicks.
ok 3 Given a click count: should render the correct number of clicks.
ok 4 Given expected props: should render the click button.
1..4
# tests 4
# pass 4
# ok
複製代碼
如今,咱們只須要實現 state 相關的邏輯和相關事件便可。
我告訴你的方法對於 ClickCounter
來講過於複雜,可是,大部分應用比這個組件更加複雜。State 常常會保存在數據庫或者在多個組件之間共享。React 社區流行的作法是先從組件本地 state 開始,而後,根據須要把 state 提高到父級組件或者全局。
事實證實,若是一開始你就使用純函數組件本地管理 state,對於之後也更易於管理。出於此緣由和其它緣由(好比:React 生命週期的混亂、state 的一致性、避免常見的bug),我更喜歡使用 reducer 管理組件 state。對於本地組件 state,你可使用 React Hook API useReducer
引入。
若是,你須要使用 state 管理框架,好比:Redux,在此以前你已經實現了一半的工做,好比:單元測試等等。
If you need to lift the state to be managed by a state manager like Redux, you’re already half way there before you even start: Unit tests and all.
(譯者注:個人理解是,若是,你一開始就使用
useReducer
本地維護 state,在須要過渡到 Redux 時更加順暢,以前的單元測試也能夠很好的重用)
首先,我爲 state reducer 建立了相應的測試文件。我將會把它放在相同目錄下,只是用了不一樣文件名。我把它命名爲 click-counter/click-counter-reducer.test.js
:
import { describe } from 'riteway';
import { reducer, click } from '../click-counter/click-counter-reducer';
describe('click counter reducer', async assert => {
assert({
given: 'no arguments',
should: 'return the valid initial state',
actual: reducer(),
expected: 0
});
});
複製代碼
我習慣以斷言開始,以確保 reducer 能夠產出一個正常的初始值。若是,你之後決定使用 Redux,它將會在沒有 state 的狀況下,調用每個 reducer,以便爲 store 初始化 state。這也使得在須要爲單元測試提供有效的初始 state 或者組件 state 時更加方便。
固然,我還須要建立相應的 reducer 文件:click-counter/click-counter-reducer.js
:
const click = () => {};
const reducer = () => {};
export { reducer, click };
複製代碼
一開始,我只是簡單的導出空的reducer 和 action creator。想知道更多有關 action creators 和 selectors 的知識,請查看:「改善 Redux 體系的 10 個提示」。今天,咱們不打算深刻探討 React/Redux 設計模式相關內容,可是,理解了這類問題,即便,你不使用 Redux 對於理解咱們今天所作的事情也有所幫助。
首先,咱們將會看到測試失敗:
# click counter reducer
not ok 5 Given no arguments: should return the valid initial state
---
operator: deepEqual
expected: 0
actual: undefined
複製代碼
如今,我來修復測試用例中的問題:
const reducer = () => 0;
複製代碼
初始化相關的測試用例如今能夠經過了,是時候添加更有意義的測試用例了:
assert({
given: 'initial state and a click action',
should: 'add a click to the count',
actual: reducer(undefined, click()),
expected: 1
});
assert({
given: 'a click count and a click action',
should: 'add a click to the count',
actual: reducer(3, click()),
expected: 4
});
複製代碼
咱們看到測試用例都失敗了(兩個分別應該返回 1
和4
的,都返回了0
)。咱們來修復它們。
注意我把 click()
做爲 reducer 的公共 API 使用。在我看來,你應該把 reducer 做爲應用的一部分,而不是直接與它交互。相反,reducer 的公共 API 應該是 action creators 和 selectors。
我沒有單獨爲 action creators 和 selectors 編寫測試用例。我老是與 reducer 相結合來測試它們。測試 reducer 也就是測試 action creators 和 selectors,反之亦然。若是,你遵照這些規則,你將會須要更少的測試用例,可是,若是,你單獨的測試它們,仍舊能夠實現一樣的測試和覆蓋率。
const click = () => ({
type: 'click-counter/click',
});
const reducer = (state = 0, { type } = {}) => {
switch (type) {
case click().type: return state + 1;
default: return state;
}
};
export { reducer, click };
複製代碼
如今,全部的單元測試都應該能夠經過:
TAP version 13
# Hello component
ok 1 Given a username: should Render a greeting to the correct username.
# ClickCounter component
ok 2 Given a click count: should render the correct number of clicks.
ok 3 Given a click count: should render the correct number of clicks.
ok 4 Given expected props: should render the click button.
# click counter reducer
ok 5 Given no arguments: should return the valid initial state
ok 6 Given initial state and a click action: should add a click to the count
ok 7 Given a click count and a click action: should add a click to the count
1..7
# tests 7
# pass 7
# ok
複製代碼
最後一步:爲組件綁定行爲事件。咱們可使用容器組件來處理。我在本地目錄中建立了一個名爲 index.js
的文件。它的內容以下:
import React, { useReducer } from 'react';
import Counter from './click-counter-component';
import { reducer, click } from './click-counter-reducer';
export default () => {
const [clicks, dispatch] = useReducer(reducer, reducer());
return <Counter clicks={ clicks } onClick={() => dispatch(click())} />; }; 複製代碼
就是這樣。這個組件只是用來管理 state,而後把 state 做爲純函數組件的 prop 向下傳遞。在瀏覽器中打開應用,點擊按鈕看是否正常運行。
到如今爲止,咱們尚未在瀏覽器中查看組件和處理樣式的問題。爲了更加的清晰,我將會在 ClickCounter
組件中添加一個標籤和一些空格。同時,也會綁定 onClick
事件。代碼以下:
import React, { Fragment } from 'react';
export default ({ clicks, onClick }) =>
<Fragment> Clicks: <span className="clicks-count">{ clicks }</span> <button className="click-button" onClick={onClick}>Click</button> </Fragment>
;
複製代碼
全部的測試用例仍是能夠經過。
容器組件的測試呢?我不會爲容器組件編寫單元測試。相反,我使用功能測試,這種測試運行在瀏覽器中或者模擬器中,用戶能夠與真實的 UI 交互,運行 end-to-end 測試。你的應用須要兩種測試(單元和功能測試),爲容器組件(那些爲了鏈接 reducer 的組件)編寫單元測試我以爲有點多餘,並且,很難實現正確的單元測試。一般,你須要模擬各類容器組件的依賴關係以即可以正常工做。
在此期間,咱們只是測試那些比較重要而不依賴反作用的組件:咱們測試了是否能夠正確的渲染,state 的管理是否正確。你仍是須要在瀏覽器中運行組件,而後查看按鈕是否正確工做。
不論是爲 React 組件實施功能/e2e測試,仍是爲其它框架實施都是相同的。詳情能夠查看 「Behavior Driven Development (BDD) and Functional Testing」。
註冊 TDD Day:可得到 5 小時有關 TDD 的高質量的視頻內容和交互課程。這是一個很棒的速成教程,能夠提升團隊的 TDD 技能。無論,你當前的 TDD 經驗如何,你都會學到更多知識。