Behavior Driven Development in ReactJS
如果想跳过前文,可以直接定位到实战篇
What is Test Driven Development?
Coding of features and tests go hand in hand.
- Write a unit test.
- Run the test. See it fail.
- Write the feature code to pass the test.
- Refactor the code.
Why TDD?
- It reduces errors and defects in the long run.
- It leads to higher quality code.
What is Behavior Driven Development?
- A variation of TDD that tests for user scenarios.
- Given, when, then… [ pattern ]
- Given notes, when deleting, then remove a note.
- BDD consists of scenarios/specifications.
Test Tools
- Jest
- Enzyme
如果想看create-react-app或jest/enzyme环境的配置,可以定位到setup内容。
我的实战
项目中Jest and Enzyme的实战。
1. 第一个Unit Test: toMatchSnapshot
快照是Jest把调用时的component的结构记录下来,下次可以用来对比结构有没有差异。
如果不一样,Jest会报错,如果是预期内的展示,可以按u
把当前快照更新为最新的snapshot。
1 | it('render correctly', () => { |
2. 测试component的state
state的初始化检测 —— 状态gifts
的值为空数组。
1 | it('init `state` for gifts as an empty list', () => { |
注意:在jest中获得state是一个state()
函数。
3. 点击交互的测试
通过className去查找交互元素,模拟用户行为,其中simulate
是Enzyme提供的模拟函数。
1 | it('add a gift to `state` when click the `add` button', () => { |
4. 利用describe划分测试代码块
用describe
把测试分组。也可以使用describe
定义一个场景,把相似的操作合并。
以下的两个测试都需要先触发一次add-gift
按钮的点击,再验证相应的测试逻辑。
下面有两个hook,beforeEach
和afterEach
,可以用来执行前置共同的action和结束之后的reset逻辑。
1 | describe('when clicking the `add-gift` button', () => { |
5. 父子组件交互测试
1)背景
在GiftGiver内,父组件<App />
根据state
中的gifts
数组渲染子组件<Gift />
,而子组件有一个删除按钮,点击后可以从父组件state
中gifts
去掉命中当前GiftID的数据项。
1 | // App.js |
2)设计思路
- 把
removeGift
挂在父组件(<App />
)上,入参giftID - 把
gift
的数据和removeGift
作为props传给子组件(<Gift />
) - 在子组件(
<Gift />
),有一个删除按钮,点击后调用父组件的callback函数,入参giftID
3)写test case的思路
I. 父组件的测试用例 App.test.js
涉及的核心逻辑或交互:负责从数据源this.state.gifts
中干掉对应数据的函数removeGift
。
测试思路:removeGift
入参giftID后,检查会不会正确地从state
中去掉该项数组(giftID === item.id)。
实现详情:
a)前置操作:模拟调用行为。
1 | beforeEach(() => { |
b)断言逻辑:确定this.state.gifts
中没有包含对应项
1 | it('gift with ID ${firstGiftID} is not in the state `gift`', () => { |
II. 子组件的测试用例 Gift.test.js
涉及的核心逻辑或交互:
- 点击一个删除按钮
- 调用父组件传过来的callback函数,并传入id
测试思路:
- 在shallow时,模拟父元素传入对应的props
- 模拟用户行为,点击删除按钮
- 检查回调函数有没有被调用,以及传入的参数对不对
实现详情:
1)在shallow时,模拟父元素传入对应的props。
1 | const mockRemove = jest.fn(); // 在第3点说明 |
2)beforeEach
里模拟删除按钮的点击
1 | beforeEach(() => { |
3)检查回调函数有没有被调用,以及传入的参数对不对
从第1点可以看到,shallow渲染传入props时,回调函数把原本的removeGift函数替换成jest的mock。(const mockRemove = jest.fn();
)
因为该方法提供了一个断言检测方法,我们可以通过这个方式,检查回调函数有没有被调用以及传入的参数是否符合预期,实际的测试语句如下。
1 | it('calls the removeGift callback', () => { |
6. coverage testing
检测实际被调用代码的覆盖程度。(冗余代码检测)
1 | npm run test -- --coverage |
指定--coverage
目标文件:,在package.json
下,添加以下语句:
1 | "jest": { |
Tips
如果存在某些函数/逻辑没有覆盖到,可以考虑新增一个和component
同级的helpers
文件夹,在里面单独写那些跟组件基本功能无关的逻辑,如用于生成ID的ID生成函数,可以单拎出来放进helpers
及进行相应的单元测试。
Setup
Preparation
- node, v8.x
- npm, v5.x
- create-react-app
Steps
I. create-react-app yourProjectName
II. install dependencies
- dependencies: react-dom & react
- devDependencies: enzyme & jest-cli
III. enzyme-adapter-react-16
In order to use the most current version of React > 16, we now need to install “enzyme adapters” to provide full compatibility with React.
1 | npm i enzyme-adapter-react-16 --save-dev |
Next, add a src/tempPolyfills.js file to create the global request animation frame function that React now depends on.
src/tempPolyfills.js should contain the following contents:
1 | const requestAnimationFrame = global.requestAnimationFrame = callback => { |
export default requestAnimationFrame;
Finally, add a src/setupTests.js file to configure the enzmye adapter for our tests. The disableLifecyleMethods portion is needed to allow us to modify props through different tests.
src/setupTests.js should contain the following contents:
1 | import requestAnimationFrame from './tempPolyfills'; |