分类: 新兴技术

组件化与UI测试

在组件化出现之前,我们不谈UI的单元测试,哪怕是对于UI页面进行测试都是一件非常困难的事情。其实组件化并不完全是为了复用,很多情况下也恰恰是为了分治,使得我们可以分组件对UI页面进行开发,然后分别对其进行单元测试。

特别是当浏览器中的Web应用越来越庞大的时候,与在后端将大型单体应用拆分成微服务架构的最佳实践一样,前端应用也可以被拆分成不同的页面和特性。

(图片来自:http://t.cn/R6UgwrH)

每个特性由一个单独的团队从端到端对其负责,它允许团队规模化地交付那些能够独立部署和维护的服务,在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 lets you render a component “one level deep” and assert facts about what its render method returns, without worrying about the behavior of child components, which are not instantiated or rendered. This does not require a 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的三种渲染方法

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)
  })
}

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)
  })
})

render(node[, options]) => CheerioWrapper

render方法则会将React组件渲染成静态的HTML字符串,返回的是一个Cheerio实例对象,采用的是一个第三方的HTML解析库Cheerio,官方的解释是「我们相信Cheerio可以非常好地处理HTML的解析和遍历,再重复造轮子只能算是一种损失」。这个CheerioWrapper可以用于分析最终结果的HTML代码结构,它的API跟shallow和mount方法的API都保持基本一致。

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)
  })
})

Enzyme 的 API 方法

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 syntax

Selectors也可以是许多其他的东西,以便于在Enzyme的wrapper中轻松地指定想要查找的节点,在下面的示例中,我们可以通过React组件构造函数的引用找到该组件,也可以基于React的displayName来查找组件,如果一个组件存在于渲染树中,其中设置了displayName并且它的第一个字符为大写字母,就能通过字符串找到它,与此同时也可以基于React组件属性的子集来查找组件和节点。

/* 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'})

测试组件的交互行为

我们不但可以通过find方法查找DOM元素,还可以通过simulate方法在组件上模拟触发某个DOM事件,比如Click,Change等等。对于浅渲染来说,事件模拟并不会像真实环境中所预期的那样进行传播,因此我们必须在一个已经设置好了事件处理方法的实际节点上调用,实际上.simulate()方法将会根据模拟的事件触发这个组件的prop。例如,.simulate(‘click’) 实际上会获取onClick prop并调用它。

Sinon则是一个可以用来Mock和Stub数据代码的第三方测试工具库,当我们需要检查一个组件当中某个特定的函数是否被调用时,我们可以使用sinon.spy()方法监视所传入该组件作为prop的onButtonClick方法,然后再通过wrapper的simulate方法模拟一个Click事件,最终验证这个被spy的onButtonClick函数是否被调用。

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
})

如何测试 React Native?

前面我们所谈论的都是如何测试使用react-dom所构建的React组件,即最终渲染的结果是浏览器当中的DOM结构,但对于React Native来说,JavaScript代码最终会被编译并用于调用iOS或Android上的Native代码,因此无法再使用基于DOM的测试工具了。

(图片来自:http://t.cn/R6UrTrG)

与此同时,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可以在大规模地减少功能测试数量上做出贡献。

(图片来自2016年11月期技术雷达)

最新一卷技术雷达将于3月28日全球发布,现在就去订阅,获悉一手技术雷达发布消息。


更多精彩洞见,请关注微信公众号:思特沃克

Share

发表评论

评论