原文地址: 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 经验如何,你都会学到更多知识。