-
Notifications
You must be signed in to change notification settings - Fork 720
About Unit Test
- 测试框架:Jest(提供一个可运行的环境、测试结构、结果报告、代码覆盖、断言、mocking 、snapshot)
- 辅助测试库 enzyme:主要用于react components render
- 辅助测试库 jsdom:在node环境提供dom渲染模拟,配合enzyme的Full Dom Reandering场景使用
- 辅助测试库 sinon:提供spy、stub、mock来做event testing、function testing,提供测试回调函数执行次数、入参判断等能力
- 模块应被渲染的DOM树是否正确
- 模块的属性传递是否正确,(属性是方法则是否被正确调用,属性是布尔值或对象则是否被正确传递且达到预期的目的)
- 模块内的各个行为响应是否正确
- @douyinfe/semi-ui 包含的各组件的代码
- 每个组件API都应该有对应的test case(除非某些API在jest环境下不好模拟,例如onScroll,允许跳过不写)
每个组件的单测用例都位于各自的__test__文件夹下,以componentName.test.js命名
- 根据组件Feature进行测试用例的编写,注意单一变量原则,每个用例只关注当前props的功能是否正常
1、添加一个测试.
import Switch from '../index';
// 此处无需再单独引入 shallow、mount、render、enzyme、sinon等
// 已经在<rootDir>/test/setup.js中统一赋值给了global,在组件级别的单测用例js中可以直接共享
describe('Switch', () => {
it('xxx should xxx', () => {
//...
});
it('unit test 2', () => {});
// ...
})
2、运行所有测试,看看新加的这个是不是失败了;如果能成功则重复步骤1.
// 只跑单元测试
npm run test:unit
// 跑所有组件的单测用例,并且输出覆盖率报告
npm run test:coverage
3、根据失败报错,有针对性的编写或改写代码。
4、再次运行测试;如果能成功则跳到步骤5,否则重复步骤3
5、重复步骤1
it('input with custom className & style', () => {
const wrapper = shallow(<Input className='test' style={{color: 'red'}}/>);
expect(wrapper.exists('.test')).toEqual(true);
expect(wrapper.find('div.test')).toHaveStyle('color', 'red');
});
// 一般检查是否拥有对应的dom节点,是否有对应的className,或者state对应的值是否正确
it('input different size', () => {
const largeInput = mount(<Input size='large'/>);
const defaultInput = mount(<Input />);
const smallInput = mount(<Input size='small' />);
expect(largeInput.find('.semi-input-large')).toHaveLength(1);
expect(smallInput.find('.semi-input-small')).toHaveLength(1);
});
it('input with placeholder', () => {
let placeholderText = 'semi placeholder';
const input = mount(<Input placeholder={placeholderText} />);
let inputDom = input.find('input');
expect(inputDom.props().placeholder).toEqual(placeholderText);
})
it('input', () => {
const input = mount(<Input />);
expect(input.state().disabled).toEqual(false); // 直接读取state
expect(input.props().disabled).toEqual(false); // 读取props
})
// 可以通过setState和setProps接口模拟组件的外部状态变化,测试组件状态动态改变时,UI是否做出了正确的响应
it('change props & state', () => {
const input = mount(<Input />);
input.setProps({ disabled: true }) ;
input.setState({ value: 1 })
input.update(); // !!!注意,需执行update()
// expect ....
}
it('Collapse with custom expandIcon / collapseIcon', () => {
let plusIcon = <Icon type="plus" />;
let minIcon = <Icon type="minus" />;
let props = {
expandIcon: plusIcon,
collapseIcon: minIcon
};
let collapse = mount(<Collapse {...props} />);
expect(collapse.props().expandIcon).toEqual(plusIcon);
expect(collapse.props().collapseIcon).toEqual(minIcon);
expect(collapse.contains(plusIcon)).toEqual(true);
expect(collapse.contains(minIcon)).toEqual(false);
});
it('input should call onChange when value change', () => {
let inputValue = 'semi';
let event = { target: { value: inputValue } };
let onChange = () => {};
// 使用sinon.spy封装回调函数,spy后可收集函数的调用信息
let spyOnChange = sinon.spy(onChange);
const input = mount(<Input onChange={spyOnChange} />);
// 找到原生input元素,触发模拟事件,模拟input的value值变化
input.find('.semi-input').simulate('change', event);
expect(spyOnChange.calledOnce).toBe(true); // onChange回调被执行一次
})
it('test callback value', () => {
// ... 主体代码同上
// 单个入参可以用calledWithMatch
expect(spyOnChange.calledWithMatch('semi')).toBe(true);
// 多个入参可以用
})
// 其他callXXX 型API的用法可以参考sinon的官方文档 https://sinonjs.org/releases/v7.5.0/spies/
it('test callback count / specific callback arguments', () => {
// ... 主体代码同上
// 获取函数的总调用次数
expect(spyOnChange.callCount).toBe(2);
// 获取该函数的第1次调用
let firstCall = spyOnChange.getCall(0);
// 第N次调用的入参
let arguments = firstCall.argus; // Array like object
})
it('collapse defaultActiveKey', () => {
const collapse = mount(<Collaplse defaultActiveKey='1'>
{...}// 中间省略
</Collapse>);
let domNode = collapse.find([tabIndex="1"]).getDOMNode();
// 使用getDOMNode获取ReactWrapper的真实DOM
// 注意某些属性的大小写,例如在dom中渲染是tabindex,但实际上获取属性时需要用tabIndex
expect(domNode.getAttribute("aria-expand")).toEqual(true);
})
it('show different direction popup layer', ()=> {
let props = {
position: 'top',
data: stringData,
defaultOpen: true,
...commonProps
};
let ac = mount(<AutoComplete {...props} />, attachTo: {...} );
expect(ac.find('.semi-popover-wrapper').instance().getAttribute('x-placement')).toEqual('top');
ac.setProps({ props: 'right' });
ac.update();
expect(ac.find('.semi-popover-wrapper').instance().getAttribute('x-placement')).toEqual('right');
})
it('optionList should show when defaultOpen is true & data not empty', () => {
// ...
// 使用find定位,children获取子节点列表,at获取第N个,getDOMNode获取DOM节点实例
let ac = mount(<AutoComplete {...props} />, { attachTo: document.getElementById('container') });
let list = ac.find('.semi-autocomplete-option-list').children();
expect(list.length).toEqual(4);
expect(list.at(0).getDOMNode().textContent).toEqual('semi');
expect(list.at(1).getDOMNode().textContent).toEqual('ies');
});
-
enzyme.smiluate 并非真正的模拟事件 https://github.com/airbnb/enzyme/issues/1606.
事件模拟不会像在真实环境中通常所期望的那样传播(即没有冒泡等机制)。因此,必须在具有事件处理程序集的实际节点上调用.simulate().
.simulate()实际上会根据你给它的事件来定位组件的prop。例如,.simulate('click')实际上会获得onClick prop并调用它. -
断言库的差异. enzyme的官方文档示例,使用的是mocha + chai。semi直接用的jest + jest自带的expect风格的断言库, 所以语法上会有差异。 如 to.have.lengthOf(1) => toHaveLength(1)
-
最外层使用了Context.Consumer的组件,不适合用shallow. Semi有些组件,需要做i18n适配,组件结构如下,如果直接用shallow渲染,只能渲染出localeConsumer这层。这种情况需要直接用mount
<LocaleConsumer>
{
(locale, localeCode) => (
<div>{...}</div>
)}
</LocaleConsumer>
- mount只将组件渲染到div元素,而不将其附加到DOM树
- 即mount默认并不会将组件挂载到document上
- 涉及弹出层的组件,例如Modal、Tooltip、Select等,需要在mount(component, { attachTo: DOM }),详情参考AutoComplete
jest 内置的 istanbul 输出的覆盖率结果, 表格中的第2列至第5列,分别对应四个衡量维度:
- 语句覆盖率(statement coverage):是否每个语句都执行了
- 分支覆盖率(branch coverage):是否每个if代码块都执行了
- 函数覆盖率(function coverage):是否每个函数都调用了
- 行覆盖率(line coverage):是否每一行都执行了
当我们执行jest 命令时,加上 --coverage后,会输出覆盖率报告
// 跑某个组件的单元测试.
type=unit jest packages/semi-ui/xxx --silent --coverage.
跑完上述命令后,jest会输出对应的html(根目录/test/converage/xxx/index.html)
点击对应组件的html,没有被执行过的行会标红高亮展示(键盘按N可以直接跳至对应行)。因此注意在你的单侧case中覆盖到这些场景后,相应的单测行覆盖度就会得到提升.