fullstact react 学习笔记unit testing (2)Enzyme
1、Enzyme的在测试React Component时的好处
(1)独立测试组件。比如测试父组件,如果子组件发生了变动,父组件的测试依然是通过的。
(2)不是测试真实的DOM,而是在虚拟DOM的基础上进行测试,因此速度比较快。
2、使用create-react-app中提供的test功能
使用npm test我们便可以对App进行测试,测试完成后并不会直接退出,而是会处于观察模式,每当代码发生变化时,变回重新进行测试。
我们仅需要保留一个测试窗口处于打开状态。
3、Enzyme的安装
(1)在cerate-react-app生成的react-scripts中并不包含Enzyme,因此我们需要在package.json中配置Enzyme.
(2)因为Enzyme是在react-test-renderer基础上开发的,因此我们也需要引入react-test-renderer。
(3)为Enzyme增加一个转换器adapter,Enzym为每个react配置了一个adapter,选择react对应的版本。
修改后的package.jason文件如下:
"devDependencies": {
"enzyme": "3.1.0",
"enzyme-adapter-react-16": "1.0.1",
"react-scripts": "1.0.14",
"react-test-renderer": "16.0.0"
},
4、Enzyme测试使用的组件App.js
import React from 'react';
class App extends React.Component {
state = {
items: [],
item: '',
};
onItemChange = (e) => {
this.setState({
item: e.target.value,
});
};
addItem = (e) => {
e.preventDefault();
this.setState({
items: this.state.items.concat(
this.state.item
),
item: '',
});
};
render() {
const submitDisabled = !this.state.item;
return(
<div
className='ui text container'
id='app'
>
<table className='ui selectable structured large table'>
<thead>
<tr>
<th>Items</th>
</tr>
</thead>
<tbody>
{
this.state.items.map((item, idx) => (
<tr
key={idx}
>
<td>{item}</td>
</tr>
))
}
</tbody>
<tfoot>
<tr>
<th>
<form
className='ui form'
onSubmit={this.addItem}
>
<div className='field'>
<input
className='prompt'
type='text'
placeholder='Add item...'
value={this.state.item}
onChange={this.onItemChange}
/>
</div>
<button
className='ui button'
type='submit'
disabled={submitDisabled}
>
Add item
</button>
</form>
</th>
</tr>
</tfoot>
</table>
</div>
)
}
}
export default App;
5、Enzyme的使用
(1)声明Enzyme使用的装换器
import Enzyme from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
Enzyme.configure({ adapter: new Adapter() });
如果我们在每个测试中引入上述语句,随着组件测试的增多,每次都要复制和粘贴,我们可以在setupTest.js中配置上述引入,这样在每次进行测试时便会自动引入上述模块
完整示例如下:
import raf from './tempPolyfills'
import Enzyme from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
Enzyme.configure({ adapter: new Adapter() });
tempPolyfills是干什么用的?它其实是在为react 16打个补丁。
(2)为react 16打个补丁
由于react 16依赖一个浏览器api 名字为requestAnimationFrame,但是在测试react 时候requestAnimationFrame并不会被自动引入。由此导致测试react失败,未来的react版本将会解决这个问题。但是现在我们还需要手工引入它。
在tempPolyfills.js中
const raf = global.requestAnimationFrame = (cb) => {
setTimeout(cb, 0);
};
export default raf;
(3)测试一个基础组件
a、引入要测试的Component
import App from './App';
b、引入React和shallow
import React from 'react';
import { shallow } from 'enzyme';
在这里我们仅用到了enzyme的shallow方法 ,因此我们显示声明一下。
c、开始测试组件
第一步:使用shallow生成shallowWrapper,包含有虚拟的DOM
第二步:使用ShallowWrapper提供的遍历和筛选方法找到我们要测试的组件内容,并进行测试该内容已经render到页面上。
第三步:模拟动作交互,测试响应的正确性。
(1)使用contains:严格包含
import App from './App'
import React from 'react'
import {shallow} from 'enzyme'
describe('App',()=>{
// 测试是否生成的table的表头th是否有items
it('测试是否生成的table的表头th是否有items',()=>{
// 首先,使用shallow生成shallowwrapper,包含了虚拟的DOM
const wrapper=shallow(<App />);
//shallowwrapper给我们提供了遍历和筛选DOM的功能,如contains
expect(wrapper.contains(<th>Items</th>)).toBe(true);
});
});
(2)使用containsMatchingElement(),近似包含
由于contains测试时包含了标签所有的属性,这里我们仅仅测试组件而忽略属性,可以使用containsMatchingElement()
it('测试是否含有<button>addItem</button>',()=>{
// 由于contains测试时包含了标签所有的属性,这里我们仅仅测试组件而忽略属性,可以使用containsMatchingElement()
const wrapper=shallow(<App />);
//shallowwrapper给我们提供了遍历和筛选DOM的功能,如contains
expect(wrapper.containsMatchingElement(<button>Add item</button>)).toBe(true);
});
});
(3)使用find遍历查找,生成一个shallowwrpper对象array
使用find返回一个shallowwrapper,类似于array拥有length属性,first()返回第一个shallowwrapper.
shallowwrapper的props方法返回一个属性对象
it('测试button的disabled属性是否为true',()=>{
// 由于contains测试时包含了标签所有的属性,这里我们仅仅测试组件而忽略属性,可以使用containsMatchingElement()
const wrapper=shallow(<App />);
// find返回一个shallowwrapper,类似于array拥有length属性,first()返回第一个shallowwrapper.
const button = wrapper.find('button').first();
//shallowwrapper的props方法返回一个属性对象
expect(button.props().disabled).toBe(true);
});
(4)使用beforeEach
上面代码中我们每一个都声明了相同的变量wrapper,我们可以将wrapper放在所有的it之前,describe之后,这样所有的it中便可以引用相同的wrapper了,免去重复声明的问题。
但是直接声明wrapper会遇到一个问题,如果在一个测试中修改了component,这样在该测试下面测试中,都将使用修改后的wrapper进行测试,这会导致测试的不可预知性。
因此,在每个测试前重新的render一下,保证所测试的组件拿到的wrapper都是一致的,最新的,这样就不会导致预测的不可知性。
beforeEach就是来干这个事情的,每次测试前都会重新的render一次,生成一个新的wrapper
示例代码如下:
let wrapper;
beforeEach(() => {
wrapper = shallow(
<App />
);
});
(5)使用simulate()模拟用户的操作
a、模拟用户输入
重新定义一个describe,以便于其他的展示组件测试分开。
describe('测试用户输入',()=>{
const item='Vancouver';
beforeEach(()=>{
const input = wrapper.find('input').first()
// 模拟用户输入
input.simulate('change',{
target:{value:item}
})
});
it('测试当用户输入时是否更新state中的item',()=>{
expect(wrapper.state().item).toBe(item);
});
it('测试当用户输入时提交框的disabled属性是否变为false',()=>{
const button = wrapper.find('button').first()
expect(button.props().disabled).toBe(false);
});
});
b、模拟用户清楚
describe('用户clear输入的内容',()=>{
beforeEach(()=>{
const input = wrapper.find('input').first()
// 模拟用户清楚内容
input.simulate('change',{
target:{value:''}
})
});
it('测试当用户清楚内容时是否更新state中的item',()=>{
expect(wrapper.state().item).toBe('');
});
it('测试当用户清楚内容时提交框的disabled属性是否变为false',()=>{
const button = wrapper.find('button').first()
expect(button.props().disabled).toBe(true);
});
});
c、模拟用户提交
describe('用户点击提交时',()=>{
// items中增加一条记录
//table中存在一条记录
// input框清空,
// submit的disabeled属性变为true
beforeEach(()=>{
const form = wrapper.find('form').first()
// 模拟用户清楚内容
form.simulate('submit',{
preventDefault:()=>{},
});
});
it('state中的items增加一条记录',()=>{
expect(wrapper.state().items).toContain(item);
});
it('table中含有一条记录',()=>{
expect(wrapper.containsMatchingElement(<td>{item}</td>)).toBe(true);
});
it('input框被清空',()=>{
const input = wrapper.find('input').first();
expect(input.props().value).toEqual('');
});
it('测试当用户提交后提交框的disabled属性是否变为true',()=>{
const button = wrapper.find('button').first()
expect(button.props().disabled).toBe(true);
});
});
通过模拟用户输入、清楚和提交我们发现要不断的声明input 和button,是否也可以将其增加到beforeEach中,以便于减少重复的声明,但这并不是可行的,因为beforeEach每次都是render最新的DOM,而我们模拟用户输入需要的input和button是在修改了DOM之后的input和button。
浙公网安备 33010602011771号