使用Enzyme测试React(Native)组件
一、使用组件化与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元素,也不会产生所谓的副作用。
二、React组件树的测试
按理来说按照纯函数这样的思路,React组件的测试应该很简单。但与此同时,对于(渲染出UI的)组件树进行测试依然存在一个问题,从下图中可以看出,越处于上层的组件,其复杂度越高。
对于***层的子组件来说,我们可以很容易的将其进行渲染并测试其逻辑正确与否,但对于较上层的父组件来说,就需要对其所包含的所有子组件都进行预先渲染,甚至于最上面的组件需要渲染出整个 UI 页面的真实DOM节点才能对其进行测试,这显然是不可取的。
浅渲染(Shallow Rendering)解决了这个问题,也就是说在我们针对某个上层组件进行测试时,源码库可以不用渲染它的子组件,所以就不用再担心子组件的表现和行为,这样就可以只对特定组件的逻辑及其渲染输出进行测试了。Facebook官方提供了react-addons-test-utils可以让我们使用浅渲染这个特性,用于测试虚拟DOM对象,即React.Component的实例。
三、使用Enzyme简化测试代码
我们常常会提到,测试代码对于复杂代码库的可维护性至关重要,但是测试代码本身的易于理解和编写,以及可读性和可维护性也同等重要。
Enzyme is a JavaScript Testing utility for React that makes it easier to assert, manipulate, and traverse your React Components output.而Enzyme则来自于活跃在JavaScript开源社区的Airbnb公司,是对官方测试工具库(react-addons-test-utils)的封装,它模拟了jQuery的API,非常直观并且易于使用和学习,提供了一些与众不同的接口和方法来减少测试的样板代码,方便你判断、操纵和遍历React Components的输出,并且减少了测试代码和实现代码之间的耦合。
Enzyme理论上应该与所有TestRunner和断言库相兼容,已经集成了多种测试类库,比如Jest、Mocha&Chai、Jasmine,不过这些不是我们今天的重点。
对比一下两者facebook/react-addons-test-utils vs airbnb/enzyme的API就一目了然,立见分明:
四、Enzyme的三种渲染方法
1. shallow(node[, options]) => ShallowWrapper
shallow方法就是对官方的Shallow Rendering的封装,浅渲染在将一个组件作为一个单元进行测试的时候非常有用,可以确保你的测试不会去间接断言子组件的行为。shallow方法只会渲染出组件的***层DOM结构,其嵌套的子组件不会被渲染出来,从而使得渲染的效率更高,单元测试的速度也会更快。
import { shallow } from enzyme describe(Enzyme Shallow, () => { it(App should have three <Todo /> components, () => { const app = shallow(<App />) expect(app.find(Todo)).to.have.length(3) }) }2. mount(node[, options]) => ReactWrapper
mount方法则会将React组件渲染为真实的DOM节点,特别是在你依赖真实的DOM结构必须存在的情况下,比如说按钮的点击事件。
完全的DOM渲染需要在全局范围内提供完整的DOM API,这也就意味着它必须在至少“看起来像”浏览器环境的环境中运行,如果不想在浏览器中运行测试,推荐使用mount的方法是依赖于一个名为jsdom的库,它本质上是一个完全在JavaScript中实现的headless浏览器。
import { mount } from enzyme describe(Enzyme Mount, () => { it(should delete Todo when click button, () => { const app = mount(<App />) const todoLength = app.find(li).length app.find(button.delete).at(0).simulate(click) expect(app.find(li).length).to.equal(todoLength - 1) }) })3. render(node[, options]) => CheerioWrapper
render方法则会将React组件渲染成静态的HTML字符串,返回的是一个Cheerio实例对象,采用的是一个第三方的HTML解析库Cheerio,官方的解释是「我们相信Cheerio可以非常好地处理HTML的解析和遍历,再重复造轮子只能算是一种损失」。
import { render } from enzyme describe(Enzyme Render, () => { it(Todo item should not have todo-done class, () => { const app = render(<App />) expect(app.find(.todo-done).length).to.equal(0) expect(app.contains(<div className="todo" />)).to.equal(true) }) })这个CheerioWrapper可以用于分析最终结果的HTML代码结构,它的API跟shallow和mount方法的API都保持基本一致。
五、Enzyme 的 API 方法
1. find() 方法与选择器
从前面的示例代码中可以看到,无论哪种渲染方式所返回的wrapper都有一个.find()方法,它接受一个selector参数,然后返回一个类型相同的wrapper对象,里面包含了所有符合条件的子组件。在这个对象的基础上,at方法则可以返回指定位置的子组件,simulate方法可以在这个组件上模拟触发某种行为。
Enzyme中的Selectors即选择器类似于CSS选择器,但是只支持非常简单的CSS选择器,如果需要支持复杂的CSS选择器,就需要引入react-dom模块的findDOMNode方法,而这是官方的TestUtils都无法提供的方式。
/* CSS Selector */ wrapper.find(.foo) //class syntax wrapper.find(input) //tag syntax wrapper.find(#foo) //id syntax wrapper.find([htmlFor="foo"]) //prop syntaxSelectors也可以是许多其他的东西,以便于在Enzyme的wrapper中轻松地指定想要查找的节点,在下面的示例中,我们可以通过React组件构造函数的引用找到该组件,也可以基于React的displayName来查找组件。
/* Component Constructor */ wrapper.find(ChildrenComponent) myComponent.displayName = ChildrenComponent wrapper.find(ChildrenComponent) /* Object Property Selector */ const wrapper = mount( <div> <span foo={ 3} bar={ false} title="baz" /> </div> ) wrapper.find({ foo: 3 }) wrapper.find({ bar: false }) wrapper.find({ title: baz})如果一个组件存在于渲染树中,其中设置了displayName并且它的***个字符为大写字母,就能通过字符串找到它,与此同时也可以基于React组件属性的子集来查找组件和节点。
2. 测试组件的交互行为
我们不但可以通过find方法查找DOM元素,还可以通过simulate方法在组件上模拟触发某个DOM事件,比如Click,Change等等。
对于浅渲染来说,事件模拟并不会像真实环境中所预期的那样进行传播,因此我们必须在一个已经设置好了事件处理方法的实际节点上调用,实际上.simulate()方法将会根据模拟的事件触发这个组件的prop。例如,.simulate(click) 实际上会获取onClick prop并调用它。
it(simulates click events, () => { const onButtonClick = sinon.spy() const wrapper = shallow( <Foo onButtonClick={ onButtonClick} /> ) wrapper.find(button).simulate(click) expect(onButtonClick.calledOnce).to.be.true })Sinon则是一个可以用来Mock和Stub数据代码的第三方测试工具库,当我们需要检查一个组件当中某个特定的函数是否被调用时,我们可以使用sinon.spy()方法监视所传入该组件作为prop的onButtonClick方法,然后再通过wrapper的simulate方法模拟一个Click事件,最终验证这个被spy的onButtonClick函数是否被调用。
六、如何测试 React Native?
前面我们所谈论的都是如何测试使用react-dom所构建的React组件,即最终渲染的结果是浏览器当中的DOM结构,但对于React Native来说,JavaScript代码最终会被编译并用于调用iOS或Android上的Native代码,因此无法再使用基于DOM的测试工具了。
与此同时,React Native还有特别多的Mobile环境依赖,所以在没有真实设备的情况下很难对其运行环境进行模拟,特别是当你希望在持续集成服务器(如Jenkins、Travis CI)运行单元测试的时候。
事实上,我们可以通过欺骗React Native让它返回常规的React组件而不是Native组件,然后就又能愉快地使用传统的JavaScript测试库来单独测试React Native组件逻辑。最基本的mock示例代码如下:
const mockComponent = (type) => { return React.createClass({ displayName: type, propTypes: { children: React.PropTypes.node }, render() { return <div { ...this.props}>{ this.props.children}</div> } }) } RN.View = mockComponent("View") RN.Text = mockComponent("Text") RN.Image = mockComponent("Image")Enzyme推荐在测试环境中使用react-native-mock这个辅助库,这是一个使用纯JavaScript将全部的React Native组件进行mock的第三方库,只需要导入这个库就可以对React Native组件进行渲染和测试。
七、总结
我们非常享受Enzyme为React.js应用提供的快速组件级UI测试功能。与许多其他基于快照的测试框架不同,Enzyme允许开发者在不进行设备渲染的情况下做测试,从而实现速度更快、粒度更小的测试。在开发React应用时,我们经常需要做大量的功能测试,而Enzyme可以在大规模地减少功能测试数量上做出贡献。
【本文是专栏作者“ThoughtWorks”的原创稿件,微信公众号:思特沃克,转载请联系原作者】
戳这里,看该作者更多好文