当前被测项目采用的是BDD模式,通过测试case根据原有业务需求的理解,对代码的质量以及主业务逻辑进行的测试case的编写。
首先是准备了待测组件button.js
:
import React, { PureComponent } from 'react';
import Empty from './../../client/components/Empty';
class Button extends PureComponent {
constructor(props) {
super(props);
this.state = {
name: ""
};
}
componentDidMount () {
if (!this.state.name) {
this.setState({
name: this.props.value
});
}
}
render() {
return (
<div>
<Empty text="无数据" />
<button {...this.props} />
</div>
);
}
}
export default Button;
为了区别shallow和render的区别,增加了一个empty的子组件, dom结构如下:
<div className='empty-view-wrapper'>
<img src={EmptyImg} />
<div className='text-content'>{text}</div>
</div>
shallow Rendering
(shallow)根据官方的说法是说,通过这种渲染方式,可以访问到React的生命周期方法。而且,shallow只能渲染当前组件,对当前组件做断言,不涉及到子组件的渲染。它的性能上最快的,大部分情况下,如果不深入组件内部测试,那么可以使用shallow渲染。
测试用例button.test.js, shallow渲染生成对应的快照对比:
import { shallow } from 'enzyme';
import toJson from 'enzyme-to-json';
test("shallow snapshot:", () => {
const wrapper = shallow(<Button {...props} />);
expect(toJson(wrapper)).toMatchSnapshot();
});
shallow snapshot
:
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Button: snapshot: 1`] = `
<div>
<Empty
text="无数据"
/>
<button
type="success"
value="提交"
/>
</div>
`;
full Rendering
(mount)它会进行完整渲染,会渲染当前组件以及所有子组件,渲染的结果和浏览器渲染结果是一样的。
测试用例button.test.js, shallow渲染生成对应的快照对比:
const wrapper = mount(<Button {...props} />);
mount snapshot
:
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Button: snapshot: 1`] = `
<Button
type="success"
value="提交"
>
<div>
<Empty
text="无数据"
>
<div
className="empty-view-wrapper"
>
<img
src="test-file-stub"
/>
<div
className="text-content"
>
无数据
</div>
</div>
</Empty>
<button
onClick={[Function]}
type="success"
value="提交"
/>
提交
</div>
</Button>
`;
static Rendering
(render)render也会进行完整渲染,但不依赖DOM API,而是渲染成HTML结构,相当于只调用了组件的render方法,得到jsx并转码为html,所以组件的生命周期方法内的逻辑都测试不到,所以render常常只用来测试一些数据(结构)一致性对比的场景。
const wrapper = render(<Button {...props} />);
render snapshot
:
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Button: snapshot: 1`] = `
<div>
<div
class="empty-view-wrapper"
>
<img
src="test-file-stub"
/>
<div
class="text-content"
>
无数据
</div>
</div>
<button
type="success"
value="提交"
/>
</div>
`;
结合button.js
组件进行展示, button.test.js
:
let props, wrapper;
beforeEach(() => {
// ...
props = {
type: 'success',
value: '提交'
};
wrapper = shallow(<Button {...props} />);
});
根据选择器查找节点,selector可以是CSS中的选择器,也可以是组件的构造函数,以及组件的display name等;
const wrapper = shallow(<Button {...props} />);
wrapper.find('button[type="success"]'); // 就能找到button这个dom节点
返回根组件的所有属性;
expect(wrapper.find('button[type="success"]').props().value).toEqual('提交');
返回根组件的指定属性;
expect(wrapper.find('button[type="success"]').prop('value')).toEqual('提交');
返回根组件的状态;
expect(wrapper.state().name).toEqual('提交');
设置根组件的状态;
const state = {
name: '先提交',
};
wrapper.setState(state);
expect(wrapper.state().name).toEqual('先提交');
设置根组件的props属性;
const newProps = {
type: 'success',
value: '提交'
};
wrapper.setProps(newProps);
expect(wrapper.find('button[type="success"]').props().value).toEqual('提交');
返回当前组件的文本内容;
const wrapper = shallow(<div><b>important</b></div>);
expect(wrapper.text()).to.equal('important');
返回当前组件的HTML代码形式;
const wrapper = shallow(<div><b>important</b></div>);
expect(wrapper.html()).to.equal('<div><b>important</b></div>');
用来模拟事件触发,event为事件名称,mock为一个event object;
对button组件稍微修改下:
constructor(props) {
super(props);
this.state = {
name: "",
count: 0 // 新增count,点击时改变count值
};
}
// 新增count变化的事件
change = () => {
this.setState({
count: 1
});
}
<button {...this.props} onClick={() => this.change()} />
测试用例:
it("simulate(): ", () => {
expect(wrapper.state().count).toEqual(0);
wrapper.find('button').simulate('click');
expect(wrapper.state().count).toEqual(1);
});
测试组件对应的 React 组件实例。
it("instance():", () => {
const inst = wrapper.instance();
expect(inst).to.be.instanceOf(Button);
});
expect({a:1}).toBe({a:1}); //判断两个对象是否相等
expect(1).not.toBe(2); //判断不等
expect(n).toBeNull(); //判断是否为null
expect(n).toBeUndefined(); //判断是否为undefined
expect(n).toBeDefined(); //判断结果与toBeUndefined相反
expect(n).toBeTruthy(); //判断结果为true
expect(n).toBeFalsy(); //判断结果为false
expect(value).toBeGreaterThan(3); //大于3
expect(value).toBeGreaterThanOrEqual(3.5); //大于等于3.5
expect(value).toBeLessThan(5); //小于5
expect(value).toBeLessThanOrEqual(4.5); //小于等于4.5
expect(value).toBeCloseTo(0.3); // 浮点数判断相等
expect('Christoph').toMatch(/stop/); //正则表达式判断
expect(['one','two']).toContain('one'); // 含有某个元素
jest.fn()
是创建Mock函数最简单的方式,如果没有定义函数内部的实现,jest.fn()
会返回undefined
作为返回值。
test('stub: ' , () => {
let mockFn = jest.fn();
let result = mockFn(1, 2, 3);
// 断言mockFn的执行后返回undefined
expect(result).toBeUndefined();
// 断言mockFn被调用
expect(mockFn).toBeCalled();
// 断言mockFn被调用了一次
expect(mockFn).toBeCalledTimes(1);
// 断言mockFn传入的参数为1, 2, 3
expect(mockFn).toHaveBeenCalledWith(1, 2, 3);
});
jest.fn()
所创建的Mock函数还可以设置返回值,定义内部实现或返回Promise
对象。
test('测试jest.fn()设置返回固定值', () => {
let mockFn = jest.fn().mockReturnValue('default');
// 断言mockFn执行后返回值为default
expect(mockFn()).toBe('default');
})
test('测试jest.fn()定义内部实现', () => {
let mockFn = jest.fn((num1, num2) => {
return num1 * num2;
})
// 断言mockFn执行后返回100
expect(mockFn(10, 10)).toBe(100);
})
test('测试jest.fn()返回Promise', async () => {
let mockFn = jest.fn().mockResolvedValue('default');
let result = await mockFn();
// 断言mockFn通过await关键字执行后返回值为default
expect(result).toBe('default');
// 断言mockFn调用后返回的是Promise对象
expect(Object.prototype.toString.call(mockFn())).toBe("[object Promise]");
})
jest.spyOn()
方法同样创建一个mock
函数,但是该mock
函数不仅能够捕获函数的调用情况,还可以正常的执行被spy
的函数。实际上,jest.spyOn()
是jest.fn()
的语法糖,它创建了一个和被spy
的函数具有相同内部代码的mock
函数。
.fn()
和.spyOn()
的一个对比:
const myObj = {
doSomething() {
console.log('does something');
}
};
test('stub .toBeCalled()', () => {
const stub = jest.fn();
stub();
expect(stub).toBeCalled();
});
test('spyOn .toBeCalled()', () => {
const somethingSpy = jest.spyOn(myObj, 'doSomething');
myObj.doSomething();
expect(somethingSpy).toBeCalled();
somethingSpy.mockRestore(); // 由于创建 spy 时,Jest 实际上修改了 myObj 对象的 doSomething 属性,所以在断言完成后,我们还要通过 mockRestore 来恢复 myObj 对象原本的 doSomething 方法
});
.fn()
和.spyOn()
的简单理解:
spy
, 重新定义原始对象的实现,并覆盖原始对象的实现,完成后,还要通过mockRestore()
恢复对象原本的方法待测组件button.js, 测试case:
let props = {
type: 'success',
value: '提交'
};
let wrapper = shallow(<Button {...props} />);
const spy = jest.spyOn(Button.prototype, 'componentDidMount');
wrapper.instance().componentDidMount(); // 实例化调用下组件
expect(Button.prototype.componentDidMount.mock.calls.length).toBe(1); // expect(spy).toHaveBeenCalledTimes(1);
expect(wrapper.state().name).toEqual('提交');
例如,我们通过setTimeOut
模拟一个回调异步,返回一个data
对象:
export const fetchData = (cb) => {
const data = {
code: 200,
msg: 'ok',
content: {
name: 'bob',
age: 20
}
};
setTimeout(function () {
return cb(data);
}, 1000)
}
默认情况下,一旦到达运行上下文底部,jest测试立即结束。这样意味着这个测试将不能按预期工作。
import { fetchData } from './fetch';
test("async test: ", () => {
const cb = (data) => {
expect(data.code).toEqual(200);
}
fetchData(cb);
});
按照上面写的测试用例,不管返回的code是不是200,都会执行成功,并不能正确按照我们的期望进行测试,问题在于一旦fetchData执行结束,此测试就在没有调用回调函数前结束。
还有另一种形式的 test,解决此问题。 使用单个参数调用 done,而不是将测试放在一个空参数的函数。 Jest会等done回调函数执行结束后,结束测试。
test("async test: ", (done) => {
const cb = (data) => {
expect(data.code).toEqual(200);
done();
}
fetchData(cb);
});
如果代码使用 Promise,还有一个更简单的方法来处理异步测试。 只需要从您的测试返回一个promise, Jest 会等待这一promise来解决。 如果promise被拒绝,则测试将自动失败。
模拟一个Promise待测请求:
export const fetchData = () => {
const data = {
code: 200,
msg: 'ok',
content: {
name: 'bob',
age: 20
}
};
return Promise.resolve(data);
}
通过expect.assertions,表示必须执行完一次expect的断言才算结束:
test("async test: ", () => {
expect.assertions(1);
fetchData().then(data => {
expect(data.code).toEqual(200);
});
});
或者通过Async/await进行测试:
test("async test: ", async () => {
const res = await fetchData();
expect(res.code).toEqual(200);
});
【参考资料】