一、组件化与UI测试在组件化出现之前,先不说UI单元测试,即使对于UI页面测试也是一件非常困难的事情。其实组件化也不完全是为了复用。很多时候,恰恰是为了分而治之,让我们可以分组件开发UI页面,然后分别进行单元测试。尤其是当浏览器中的Web应用越来越大时,就像后端将大型单体应用拆分成微服务架构的最佳实践一样,前端应用也可以拆分成不同的页面和特性。每个功能都由一个端到端的独立团队拥有,这使团队能够大规模交付可独立部署和维护的服务。在2016年11月号的技术雷达中,这种方法被称为微观。微前端的目标是让Web应用程序的功能相互独立,以便每个功能都可以独立开发、测试和部署。React.js是前端框架的后起之秀,但在2015年凭借虚拟DOM、组件化、单向数据流等利器,掀起了前端UI构建的新功能风潮。组件化虽然不是React***提出来的,但是在前端界被React发扬光大,现在几乎所有的所谓现代UI框架比如Angular或者Vue都采用了组件化作为框架的基础。React使UI测试变得更加容易。React组件可以简化为这样一个表达式,即UI=f(data)。这个纯函数只返回一个描述UI组件应该是什么样子的虚拟DOM。本质上是一个树型数据结构。向这个纯函数输入一些应用状态,你会得到相应的UI描述的输出。这个过程不会直接操作实际的UI元素,也不会产生所谓的副作用。2、React组件树的测试按道理来说,按照纯函数的思想,React组件的测试应该是非常简单的。但与此同时,测试组件树(渲染UI)仍然存在问题。从下图可以看出,组件越高,它的复杂度就越高。对于顶层的子组件,我们可以很容易地渲染它,测试它的逻辑是否正确,但是对于上层的父组件,我们需要实现它包含的所有子组件的预渲染,甚至是topmost组件需要渲染整个UI页面的真实DOM节点才能对其进行测试,这显然是不可取的。浅层渲染让您可以渲染组件“一层深度”并断言其渲染方法返回的事实,而无需担心未实例化或渲染的子组件的行为。这不需要DOM。浅层渲染(ShallowRendering)解决了这个问题,也就是说,当我们测试某个上层组件时,我们不需要渲染它的子组件,所以我们不必担心性能和行为子组件的数量,这样我们就只能测试逻辑及其呈现的输出。Facebook官方提供了react-addons-test-utils,可以让我们使用浅层渲染来测试虚拟DOM对象,即React.Component的实例。3、使用Enzyme简化测试代码我们经常提到测试代码对于复杂代码库的可维护性至关重要,但测试代码本身易于理解和编写,可读性和可维护性同样重要。Enzyme是一个用于React的JavaScript测试实用程序,可以更轻松地断言、操作和遍历React组件的输出。Enzyme来自Airbnb,活跃于JavaScript开源社区,是官方的测试工具库(react-addons-test-utils),模拟jQueryAPI,非常直观易用易学,提供一些有特色的接口和方法,减少测试的样板代码,让你可以判断、操作和遍历ReactComponents的输出,降低测试代码和实现代码之间的耦合度。理论上Enzyme应该兼容所有的TestRunner和断言库,并且已经集成了多种测试库,比如Jest、Mocha&Chai、Jasmine,不过这些不是我们今天的重点。对比一下facebook/react-addons-test-utilsvsairbnb/enzyme的两个API,一目了然,一目了然:4.Enzyme的三种渲染方式1.shallow(node[,options])=>ShallowWrappershallowmethod是对官方的ShallowRendering包,浅渲染在测试一个组件作为一个单元时非常有用,确保你的测试不会间接断言子组件的行为。shallow方法只会渲染组件的一级DOM结构,不会渲染其嵌套的子组件,这样渲染效率更高,单元测试速度更快。import{shallow}from'enzyme'describe('EnzymeShallow',()=>{it('App应该有三个组件',()=>{constapp=shallow()expect(app.find('Todo')).to.have.length(3)})}2.mount(node[,options])=>ReactWrappermount方法会将React组件渲染为真正的DOM节点,特别是如果你依赖real在DOM结构必须存在的情况下,比如按钮点击事件。完整的DOM渲染需要完整的DOMAPI在全局可用,这意味着它必须在至少“看起来像”浏览器环境的环境中运行,如果您不想在浏览器中运行测试,建议使用mount的方式是依赖一个名为jsdom的库,它本质上是一个完全用JavaScript实现的无头浏览器。import{mount}from'enzyme'describe('EnzymeMount',()=>{it('shoulddeleteTodowhenclickbutton',()=>{constapp=mount()consttodoLength=app.find('li')。lengthapp.find('button.delete').at(0).simulate('click')expect(app.find('li').length).to.equal(todoLength-1)})})3.render(node[,options])=>CheerioWrapperrender方法将React组件渲染成静态HTML字符串,并返回一个Cheerio实例对象,使用第三方HTML解析库Cheerio。官方的解释是“我们相信Cheerio能够很好的处理HTML的解析和遍历,重新发明轮子只能算是一种损失。”import{render}from'enzyme'describe('EnzymeRender',()=>{it('Todoitemshouldnothavetodo-doneclass',()=>{constapp=render()expect(app.find('.todo-done').length).to.equal(0)expect(app.contains()).to.equal(true)})})这个CheerioWrapper可以用来分析最终resultHTML代码结构,其API与shallow和mount方法的API基本一致五、Enzyme的API方法1.find()方法和selector从前面的示例代码可以看出,无论哪种渲染方法返回wrapper,有一个.find()方法,它接受一个selector参数,然后返回一个相同类型的wrapper对象,这个对象包含了所有符合条件的子组件。在这个对象的基础上,at方法可以返回指定位置的子组件,simulate方法可以在这个组件上模拟并触发某种行为。Enzyme中的选择器类似于CSS选择器,但只支持非常简单的CSS选择器。如果需要支持复杂的CSS选择器,需要引入react-dom模块的findDOMNode方法,这是官方的TestUtils方式没有的。/*CSSSelector*/wrapper.find('.foo')//classsyntaxwrapper.find('input')//tagsyntaxwrapper.find('#foo')//idsyntaxwrapper.find('[htmlFor="foo"]')//propsyntaxSelectors也可以是很多其他的东西,这样你就可以方便的在Enzyme的wrapper中指定你要查找的节点。在下面的例子中,我们可以通过React组件构造器的引用来查找组件,也可以根据React的displayName来查找组件。/*ComponentConstructor*/wrapper.find(ChildrenComponent)myComponent.displayName='ChildrenComponent'wrapper.find('ChildrenComponent')/*ObjectPropertySelector*/constwrapper=mount(
)wrapper.find({foo:3})wrapper.find({bar:false})wrapper.find({title:'baz'})如果组件存在于渲染中tree,其中设置了displayName且第一个字符为大写字母,可以通过字符串查找,也可以根据React组件属性的子集查找组件和节点。2、测试组件的交互行为,我们不仅可以通过find方法找到DOM元素,还可以通过simulate方法模拟并触发组件上的某个DOM事件,比如Click、Change等。对于浅层渲染,事件模拟在真实环境中不会像预期的那样传播,所以我们必须在已经设置了事件处理方法的实际节点上调用它。事实上,.simulate()方法会根据模拟的事件触发这个组件的prop。例如,.simulate('click')实际上会获取onClick属性并调用它。it('simulateclickevents',()=>{constonButtonClick=sinon.spy()constwrapper=shallow(
)wrapper.find('button').simulate('click')expect(onButtonClick.calledOnce).to.be.true})Sinon是第三方测试工具库,可用于Mock和Stub数据代码。当我们需要检查组件中的某个特定函数是否被调用时,我们可以使用sinon.spy()方法监听作为prop传入的组件的onButtonClick方法,然后通过wrapper的simulate方法模拟一个Click事件,最后验证间谍的onButtonClick函数是否被调用。6.如何测试ReactNative?我们讲了如何测试使用react-dom构建的React组件,即最终渲染结果是浏览器中的DOM结构,但是对于ReactNative来说,JavaScript代码最终会被编译并用于调用Native代码在iOS或Android上,因此不能再使用基于DOM的测试工具。同时ReactNative对Mobile环境也有很多依赖,没有真机很难模拟它的运行环境,尤其是当你想在持续集成服务器(比如Jenkins,TravisCI)在测试时。事实上,我们可以欺骗ReactNative返回常规的React组件而不是Native组件,然后愉快地使用传统的JavaScript测试库来隔离地测试ReactNative组件逻辑。最基本的mock示例代码如下:constmockComponent=(type)=>{returnReact.createClass({displayName:type,propTypes:{children:React.PropTypes.node},render(){return
{this.props.children}
}})}RN.View=mockComponent("View")RN.Text=mockComponent("Text")RN.Image=mockComponent("Image")Enzyme推荐在测试环境中,使用辅助库react-native-mock,这是一个使用纯JavaScript来mock所有ReactNative组件的第三方库。你只需要导入这个库来渲染和测试ReactNative组件。7.总结我们非常喜欢Enzyme对React.js应用程序的快速组件级UI测试功能。与许多其他基于快照的测试框架不同,Enzyme允许开发人员在没有设备渲染的情况下进行测试,从而实现更快、更精细的测试。在开发React应用程序时,我们经常需要做大量的功能测试,而Enzyme可以帮助大规模减少功能测试的数量。【本文为专栏作者“ThoughtWorks”原创稿件,微信公众号:Thinkworker,转载请联系原作者】点此查看该作者更多好文