如果想跳过前文,可以直接定位到 实战篇
Coding of features and tests go hand in hand.
如果想看 create-react-app 或 jest/enzyme 环境的配置,可以 定位到 setup 内容。
项目中 Jest and Enzyme 的实战。
toMatchSnapshot
快照是 Jest 把调用时的 component 的结构记录下来,下次可以用来对比结构有没有差异。
如果不一样,Jest 会报错,如果是预期内的展示,可以按 u
把当前快照更新为最新的 snapshot。
it('render correctly', () => {
expect(app).toMatchSnapshot();
});
state 的初始化检测 —— 状态 gifts
的值为空数组。
it('init `state` for gifts as an empty list', () => {
expect(app.state().gifts).toEqual([]);
});
注意:在 jest 中获得 state 是一个 state()
函数。
通过 className 去查找交互元素,模拟用户行为,其中 simulate
是 Enzyme 提供的模拟函数。
it('add a gift to `state` when click the `add` button', () => {
app.find('.btn-add').simulate('click');
expect(app.state().gifts.length).not.toBe(0);
});
// 检验某个 component(Gift)是否存在
it('create a Gift component', () => {
expect(app.find(Gift).exists()).toBe(true);
});
用 describe
把测试分组。也可以使用describe
定义一个场景,把相似的操作合并。
以下的两个测试都需要先触发一次 add-gift
按钮的点击,再验证相应的测试逻辑。
下面有两个 hook,beforeEach
和 afterEach
,可以用来执行前置共同的 action和结束之后的 reset 逻辑。
describe('when clicking the `add-gift` button', () => {
beforeEach(() => {
app.find('.btn-add').simulate('click');
});
afterEach(() => {
// reset state `gifts` to []
app.setState({
gifts: []
});
});
it('add a gift to `state`', () => {
// app.find('.btn-add').simulate('click');
expect(app.state().gifts.length).not.toBe(0);
});
it('display gifts on the rendered list', () => {
// app.find('.btn-add').simulate('click');
const stateListLength = app.state().gifts.length;
const listItemLength = app.find('.list-item').length;
expect(stateListLength).toEqual(listItemLength);
});
});
在 GiftGiver 内,父组件<App />
根据 state
中的gifts
数组渲染子组件<Gift />
,而子组件有一个删除按钮,点击后可以从父组件 state
中gifts
去掉命中当前 GiftID 的数据项。
// App.js
// state
this.state.gifts = [{
id: xxx
}]
removeGift(id) {
// this.state.gifts.filter(gift => gift.id !== id)
}
// render
{ this.state.gifts.map(gift => (
<Gift gift={gift} removeGift={removeGift} />
))}
// Gift.js
// render
// const { gift, removeGift } = this.props;
<div>
<Button onClick={ gift => removeGift(gift.id) }>remove</Button>
</div>
removeGift
挂在父组件(<App />
)上,入参 giftIDgift
的数据和removeGift
作为 props 传给子组件(<Gift />
)<Gift />
),有一个删除按钮,点击后调用父组件的 callback 函数,入参 giftID涉及的核心逻辑或交互:负责从数据源 this.state.gifts
中干掉对应数据的函数removeGift
。
测试思路:removeGift
入参 giftID 后,检查会不会正确地从 state
中去掉该项数组(giftID === item.id)。
实现详情:
a)前置操作:模拟调用行为。
beforeEach(() => {
// call the `removeGift` function in App.js
app.instance().removeGift(firstGiftID);
});
b)断言逻辑:确定 this.state.gifts
中没有包含对应项
it('gift with ID ${firstGiftID} is not in the state `gift`', () => {
const { gifts } = app.state();
const targetGiftList = gifts.find(gift => gift.id === firstGiftID) || [];
expect(targetGiftList.length).toBe(0);
});
涉及的核心逻辑或交互:
点击一个删除按钮
调用父组件传过来的 callback 函数,并传入 id 测试思路:
在 shallow 时,模拟父元素传入对应的 props
模拟用户行为,点击删除按钮
检查回调函数有没有被调用,以及传入的参数对不对 实现详情:
1)在 shallow 时,模拟父元素传入对应的 props。
const mockRemove = jest.fn(); // 在第 3 点说明
const giftID = 1;
const props = {
gift: {
id: giftID
},
removeGift: mockRemove
};
const gift = shallow(<Gift { ...props } />);
2)beforeEach
里模拟删除按钮的点击
beforeEach(() => {
gift.find('.btn-delete').simulate('click');
});
3)检查回调函数有没有被调用,以及传入的参数对不对
从第 1 点可以看到,shallow 渲染传入 props 时,回调函数把原本的 removeGift 函数替换成 jest 的 mock。(const mockRemove = jest.fn();
)
因为该方法提供了一个断言检测方法,我们可以通过这个方式,检查回调函数有没有被调用以及传入的参数是否符合预期,实际的测试语句如下。
it('calls the removeGift callback', () => {
expect(mockRemove).toHaveBeenCalledWith(giftID);
});
检测实际被调用代码的覆盖程度。(冗余代码检测)
npm run test -- --coverage
指定 --coverage
目标文件:,在package.json
下,添加以下语句:
"jest": {
"collectCoverageFrom": [
"src/**.js",
"!src/index.js"
]
}
如果存在某些函数/逻辑没有覆盖到,可以考虑新增一个和 component
同级的helpers
文件夹,在里面单独写那些跟组件基本功能无关的逻辑,如用于生成 ID 的 ID 生成函数,可以单拎出来放进helpers
及进行相应的单元测试。
I. create-react-app yourProjectName
II. install dependencies
In order to use the most current version of React > 16, we now need to install “enzyme adapters” to provide full compatibility with React.
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:
const requestAnimationFrame = global.requestAnimationFrame = callback => {
setTimeout(callback, 0);
}
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:
import requestAnimationFrame from './tempPolyfills';
import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
configure({ adapter: new Adapter(), disableLifecycleMethods: true });