React 测试入门教程

作者: 阿里云云栖号 | 来源:发表于2017-04-24 13:59 被阅读176次

    点击右侧姓名查看本文作者更多内容:阮一峰

    越来越多的人,使用React开发Web应用。它的测试就成了一个大问题。

    React的组件结构和JSX语法,不适用传统的测试工具,必须有新的测试方法和工具。

    本文总结目前React测试的基本做法和最佳实践,手把手教你如何写React测试。

    一、Demo应用

    请先安装Demo

    $gitclone https://github.com/ruanyf/react-testing-demo.git$cdreact-testing-demo&&npminstall$ npm start

    然后,打开 http://127.0.0.1:8080/,你会看到一个 Todo 应用。

    接下来,我们就要测试这个应用,一共有5个测试点。

    应用标题应为"Todos"

    Todo项的初始状态("未完成"或"已完成")应该正确

    点击一个Todo项,它就反转状态("未完成"变为"已完成",反之亦然)

    点击删除按钮,该Todo项就被删除

    点击添加按钮,会新增一个Todo项

    这5个测试用例都已经写好了,执行一下就可以看到结果。

    $ npmtest

    下面就来看,测试用例应该怎么写。测试框架我用的是Mocha,如果你不熟悉,可以先读我写的《Mocha教程》

    二、测试工具库

    React测试必须使用官方的测试工具库,但是它用起来不够方便,所以有人做了封装,推出了一些第三方库,其中Airbnb公司的Enzyme最容易上手。

    这就是说,同样的测试用例至少有两种写法,本文都将介绍。

    官方测试工具库的写法

    Enzyme的写法

    三、官方测试工具库

    我们知道,一个React组件有两种存在形式:虚拟DOM对象(即React.Component的实例)和真实DOM节点。官方测试工具库对这两种形式,都提供测试解决方案。

    Shallow Rendering:测试虚拟DOM的方法

    DOM Rendering: 测试真实DOM的方法

    3.1 Shallow Rendering

    Shallow Rendering (浅渲染)指的是,将一个组件渲染成虚拟DOM对象,但是只渲染第一层,不渲染所有子组件,所以处理速度非常快。它不需要DOM环境,因为根本没有加载进DOM。

    首先,在测试脚本之中,引入官方测试工具库。

    importTestUtilsfrom'react-addons-test-utils';

    然后,写一个 Shallow Rendering 函数,该函数返回的就是一个浅渲染的虚拟DOM对象。

    importTestUtilsfrom'react-addons-test-utils';functionshallowRender(Component){constrenderer=TestUtils.createRenderer();renderer.render();returnrenderer.getRenderOutput();}

    第一个测试用例,是测试标题是否正确。这个用例不需要与DOM互动,不涉及子组件,所以使用浅渲染非常合适。

    describe('Shallow Rendering',function(){it('App\'s title should be Todos',function(){constapp=shallowRender(App);expect(app.props.children[0].type).to.equal('h1');expect(app.props.children[0].props.children).to.equal('Todos');});});

    上面代码中,const app = shallowRender(App)表示对App组件进行"浅渲染",然后app.props.children[0].props.children就是组件的标题。

    你大概会觉得,这个属性的写法太古怪了,但实际上是有规律的。每一个虚拟DOM对象都有props.children属性,它包含一个数组,里面是所有的子组件。app.props.children[0]就是第一个子组件,在我们的例子中就是h1元素,它的props.children属性就是h1的文本。

    第二个测试用例,是测试Todo项的初始状态。

    首先,需要修改shallowRender函数,让它接受第二个参数。

    importTestUtilsfrom'react-addons-test-utils';functionshallowRender(Component,props){constrenderer=TestUtils.createRenderer();renderer.render();returnrenderer.getRenderOutput();}

    下面就是测试用例。

    importTodoItemfrom'../app/components/TodoItem';describe('Shallow Rendering',function(){it('Todo item should not have todo-done class',function(){consttodoItemData={id:0,name:'Todo one',done:false};consttodoItem=shallowRender(TodoItem,{todo:todoItemData});expect(todoItem.props.children[0].props.className.indexOf('todo-done')).to.equal(-1);});});

    上面代码中,由于TodoItemApp的子组件,所以浅渲染必须在TodoItem上调用,否则渲染不出来。在我们的例子中,初始状态反映在组件的Class属性(props.className)是否包含todo-done。

    3.2 renderIntoDocument

    官方测试工具库的第二种测试方法,是将组件渲染成真实的DOM节点,再进行测试。这时就需要调用renderIntoDocument方法。

    importTestUtilsfrom'react-addons-test-utils';importAppfrom'../app/components/App';constapp=TestUtils.renderIntoDocument();

    renderIntoDocument方法要求存在一个真实的DOM环境,否则会报错。因此,测试用例之中,DOM环境(即window,document和navigator对象)必须是存在的。jsdom库提供这项功能。

    importjsdomfrom'jsdom';if(typeofdocument==='undefined'){global.document=jsdom.jsdom('');global.window=document.defaultView;global.navigator=global.window.navigator;}

    将上面这段代码,保存在test子目录下,取名为setup.js。然后,修改package.json。

    {"scripts":{"test":"mocha --compilers js:babel-core/register --require ./test/setup.js",},}

    现在,每次运行npm test,setup.js就会包含在测试脚本之中一起执行。

    第三个测试用例,是测试删除按钮。

    describe('DOM Rendering',function(){it('Click the delete button, the Todo item should be deleted',function(){constapp=TestUtils.renderIntoDocument();lettodoItems=TestUtils.scryRenderedDOMComponentsWithTag(app,'li');lettodoLength=todoItems.length;letdeleteButton=todoItems[0].querySelector('button');TestUtils.Simulate.click(deleteButton);lettodoItemsAfterClick=TestUtils.scryRenderedDOMComponentsWithTag(app,'li');expect(todoItemsAfterClick.length).to.equal(todoLength-1);});});

    上面代码中,第一步是将App渲染成真实的DOM节点,然后使用scryRenderedDOMComponentsWithTag方法找出app里面所有的li元素。然后,取出第一个li元素里面的button元素,使用TestUtils.Simulate.click方法在该元素上模拟用户点击。最后,判断剩下的li元素应该少了一个。

    这种测试方法的基本思路,就是找到目标节点,然后触发某种动作。官方测试工具库提供多种方法,帮助用户找到所需的DOM节点。

    scryRenderedDOMComponentsWithClass:找出所有匹配指定className的节点

    findRenderedDOMComponentWithClass:与scryRenderedDOMComponentsWithClass用法相同,但只返回一个节点,如有零个或多个匹配的节点就报错

    scryRenderedDOMComponentsWithTag:找出所有匹配指定标签的节点

    findRenderedDOMComponentWithTag:与scryRenderedDOMComponentsWithTag用法相同,但只返回一个节点,如有零个或多个匹配的节点就报错

    scryRenderedComponentsWithType:找出所有符合指定子组件的节点

    findRenderedComponentWithType:与scryRenderedComponentsWithType用法相同,但只返回一个节点,如有零个或多个匹配的节点就报错

    findAllInRenderedTree:遍历当前组件所有的节点,只返回那些符合条件的节点

    可以看到,上面这些方法很难拼写,好在还有另一种找到DOM节点的替代方法。

    3.3 findDOMNode

    如果一个组件已经加载进入DOM,react-dom模块的findDOMNode方法会返回该组件所对应的DOM节点。

    我们使用这种方法来写第四个测试用例,用户点击Todo项时的行为。

    import{findDOMNode}from'react-dom';describe('DOM Rendering',function(done){it('When click the Todo item,it should become done',function(){constapp=TestUtils.renderIntoDocument();constappDOM=findDOMNode(app);consttodoItem=appDOM.querySelector('li:first-child span');letisDone=todoItem.classList.contains('todo-done');TestUtils.Simulate.click(todoItem);expect(todoItem.classList.contains('todo-done')).to.be.equal(!isDone);});});

    上面代码中,findDOMNode方法返回App所在的DOM节点,然后找出第一个li节点,在它上面模拟用户点击。最后,判断classList属性里面的todo-done,是否出现或消失。

    第五个测试用例,是添加新的Todo项。

    describe('DOM Rendering',function(done){it('Add an new Todo item, when click the new todo button',function(){constapp=TestUtils.renderIntoDocument();constappDOM=findDOMNode(app);lettodoItemsLength=appDOM.querySelectorAll('.todo-text').length;letaddInput=appDOM.querySelector('input');addInput.value='Todo four';letaddButton=appDOM.querySelector('.add-todo button');TestUtils.Simulate.click(addButton);expect(appDOM.querySelectorAll('.todo-text').length).to.be.equal(todoItemsLength+1);});});

    上面代码中,先找到input输入框,添加一个值。然后,找到Add Todo按钮,在它上面模拟用户点击。最后,判断新的Todo项是否出现在Todo列表之中。

    findDOMNode方法的最大优点,就是支持复杂的CSS选择器。这是TestUtils本身不提供的。

    四、Enzyme库

    Enzyme是官方测试工具库的封装,它模拟了jQuery的API,非常直观,易于使用和学习。

    它提供三种测试方法。

    shallow

    render

    mount

    4.1 shallow

    shallow方法就是官方的shallow rendering的封装。

    下面是第一个测试用例,测试App的标题。

    import{shallow}from'enzyme';describe('Enzyme Shallow',function(){it('App\'s title should be Todos',function(){letapp=shallow();expect(app.find('h1').text()).to.equal('Todos');});};

    上面代码中,shallow方法返回App的浅渲染,然后app.find方法找出h1元素,text方法取出该元素的文本。

    关于find方法,有一个注意点,就是它只支持简单选择器,稍微复杂的一点的CSS选择器都不支持。

    component.find('.my-class');// by class namecomponent.find('#my-id'); // by idcomponent.find('td');// by tagcomponent.find('div.custom-class');// by compound selectorcomponent.find(TableRow);// by constructorcomponent.find('TableRow');// by display name

    4.2 render

    render方法将React组件渲染成静态的HTML字符串,然后分析这段HTML代码的结构,返回一个对象。它跟shallow方法非常像,主要的不同是采用了第三方HTML解析库Cheerio,它返回的是一个Cheerio实例对象。

    下面是第二个测试用例,测试所有Todo项的初始状态。

    import{render}from'enzyme';describe('Enzyme Render',function(){it('Todo item should not have todo-done class',function(){letapp=render();expect(app.find('.todo-done').length).to.equal(0);});});

    在上面代码中,你可以看到,render方法与shallow方法的API基本是一致的。 Enzyme的设计就是,让不同的底层处理引擎,都具有同样的API(比如find方法)。

    4.3 mount

    mount方法用于将React组件加载为真实DOM节点。

    下面是第三个测试用例,测试删除按钮。

    import{mount}from'enzyme';describe('Enzyme Mount',function(){it('Delete Todo',function(){letapp=mount();lettodoLength=app.find('li').length;app.find('button.delete').at(0).simulate('click');expect(app.find('li').length).to.equal(todoLength-1);});});

    上面代码中,find方法返回一个对象,包含了所有符合条件的子组件。在它的基础上,at方法返回指定位置的子组件,simulate方法就在这个组件上触发某种行为。

    下面是第四个测试用例,测试Todo项的点击行为。

    import{mount}from'enzyme';describe('Enzyme Mount',function(){it('Turning a Todo item into Done',function(){letapp=mount();lettodoItem=app.find('.todo-text').at(0);todoItem.simulate('click');expect(todoItem.hasClass('todo-done')).to.equal(true);});});

    下面是第五个测试用例,测试添加新的Todo项。

    import{mount}from'enzyme';describe('Enzyme Mount',function(){it('Add a new Todo',function(){letapp=mount();lettodoLength=app.find('li').length;letaddInput=app.find('input').get(0);addInput.value='Todo Four';app.find('.add-button').simulate('click');expect(app.find('li').length).to.equal(todoLength+1);});});

    4.4 API

    下面是Enzyme的一部分API,你可以从中了解它的大概用法。

    .get(index):返回指定位置的子组件的DOM节点

    .at(index):返回指定位置的子组件

    .first():返回第一个子组件

    .last():返回最后一个子组件

    .type():返回当前组件的类型

    .text():返回当前组件的文本内容

    .html():返回当前组件的HTML代码形式

    .props():返回根组件的所有属性

    .prop(key):返回根组件的指定属性

    .state([key]):返回根组件的状态

    .setState(nextState):设置根组件的状态

    .setProps(nextProps):设置根组件的属性

    (完)

    相关文章

      网友评论

        本文标题:React 测试入门教程

        本文链接:https://www.haomeiwen.com/subject/obcpzttx.html