测试环境,集成测试

测试你的前端代码 – part4(集成测试)

2017/06/05 · 基础技术 · 测试

原文出处: Gil Tayar   译文出处:胡子大哈   

上一篇文章《测试你的前端代码 – part3(端到端测试)》中,我介绍了关于端到端测试的基本知识,从本文介绍集成测试(Integration Testing)。

1.为什么要使用单元测试工具?

因为代码之间的相互调用关系,又希望测试过程单元相互独立,又能正常运行,这就需要我们对被测函数的依赖函数和环境进行mock,在测试数据输入、测试执行和测试结果检查方面存在很多相似性,测试工具正是为我们在这些方面提供了方便。

所谓单元测试也就是对每个单元进行测试,通俗的将一般针对的是函数,类或单个组件,不涉及系统和集成。单元测试是软件测试的基础测试。

集成测试

我们已经看过了“测试光谱”中的两种测试:单元测试和端到端测试。实际工作中的测试经常是介于这两种测试之间的,包括我在内的大多数人通常把这种测试叫做集成测试。

2.React 的标配测试工具 Jest。

Jest主要有以下特点:

      1.适应性:Jest是模块化、可扩展和可配置的。

     2.沙箱和快速:Jest虚拟化了JavaScript的环境,能模拟浏览器,并且并行执行

     3.快照测试:Jest能够对React 树进行快照或别的序列化数值快速编写测试,提供快速更新的用户体验。

     4.支持异步代码测试:支持promises和async/await

     5.自动生成静态分析结果:不仅显示测试用例执行结果,也显示语句、分支、函数等覆盖率。

JEST对比Mocha来说,因为如下几个优点最后胜出:

     1.和React师出同门,FB官方支持

     2.已经集成了测试覆盖率检查、mock等功能,不需要安装额外的库

     3.文档完备,官方提供了和babel、webpack集成情况下以及异步调用的测试解决方案

     4.官方提供snapshot testing解决方案

关于术语

和许多 TDD 爱好者聊过以后,我了解了他们对“集成测试”这个词有一些不同的理解。他们认为集成测试是测试代码边界,即代码对外的接口部分。

比如他们代码中有 Ajax,localStorage 或者 IndexedDB 操作,那其代码就不能做单元测试,这时他们会把这些代码打包成接口,然后在做单元测试的时候 mock 这些接口。当真正测试这些接口的时候才称作“集成测试”。从这个角度来说,“集成测试”就是在纯的单元测试以外,测试与外部“真实世界”相关的代码。

而我和其他一些人则倾向于认为“集成测试”是将两个或多个单元测试综合起来进行测试的一种方法。通过接口把与外部相关的代码打包到一起,再 mock,只是其中的一种实现方式。

我的观点里,决定是否使用真实场景的 Ajax 或者其他 I/O 操作进行集成测试(即不使用 mock),取决于是否能够保证测试速度足够快,并且能够稳定测试(不发生 flaky 的情况)。如果可以确定这样的话,那尽管用真实场景进行集成测试就好了。不过如果很慢或者发生不稳定测试的情况,那还是用 mock 会好一些。

在我们的例子中,计算器应用唯一的真实 I/O 就是操作 DOM 了,没有 Ajax 调用,所以不存在上面的问题。

3.Jest + Enzyme的使用过程

1.安装

$ nam install —save-dev jest

如果需要在测试项目中使用babel,需要安装babel-jest

$ nam install —save-dev babel-jest

然后安装enzyme

$ npm install enzyme —save-dev

如果使用的react版本在13以上,还需要安装react-addons-test-utils

$ nam i —save-dev react-addons-test-utils

 

2.配置

JEST运行基础功能虽然无需配置,但是官方依然提供了配置选项来实现个性化需求。

package.json文件中配置jest的collectCoverageFrom参数,来指定检查所有需要测试的文件(无论源文件有没有被测试文件使用到)

coverageThreshold 参数来配置测试覆盖率。

 

jest.config.js 配置jest:

module.exports = {

  bail: true, //遇上 test feature, 则Stop running test, 默认值是false

  cacheDirectory: './node_modules/.cache', //测试缓存数据的存储位置

  testEnvironment: 'jsdom', //default brower-like enviroment, 如果你搭建了一个node service node-like enviroment

  coverageThreshold: { //测试覆盖率, 阈值不满足,就返回测试失败

    global: {

      branches: 90,

      functions: 90,

      lines: 90,

      statements: 90,

    },

  },

  coveragePathIgnorePatterns: [ //该路径下的测试,忽略在测试覆盖率上

    'build',

    '<rootDir>/src/shared/libs/url/',

  ],

  testRegex: 'test/.*\.jsx?$', //要测试的文件目录及后缀

  testPathIgnorePatterns: [ //忽略该路径的文件测试

    '<rootDir>/node_modules/',

    '<rootDir>/build/',

    '<rootDir>/scripts/',

    '<rootDir>/api/',

    '<rootDir>/test/setup.js',

    '__mocks__',

  ],

  moduleFileExtensions: ['', 'json', 'js', 'jsx', 'less'], //测试模块中用到的文件的后缀名配置

  modulePaths: ['<rootDir>/src', '<rootDir>'],

  moduleNameMapper: {  //与测试无关的资源文件同意mock 掉,这样在import 的时候就不会真的引入这些文件

    '^import?': '<rootDir>/build/jestImportMock.js',

    '\.(css|less|gif|jpg|jpeg|png)$': '<rootDir>/build/jestStyleMock.js',

  },

  setupFiles: ['<rootDir>/test/setup.js'], //给每个测试文件添加额外的配置

  transformIgnorePatterns: [ //测试过程不改变满足配置的文件

    '<rootDir>/node_modules/(?!(react-aaui|tempest\.js)/)',

    'babel-runtime',

  ],

}

 

mock DOM

这就引出了一个问题:在集成测试中是否需要 mock DOM?重新思考一下上面我说的标准,使用真实 DOM 是否会使测试变慢呢,答案是会的。使用真实 DOM 意味着要用浏览器,用浏览器意味着测试速度变慢,测试变的不稳定。

那么是不是要么只能尽量把操作 DOM 的代码分离出来,要么只能使用端到端测试了呢?其实这两种方法都不好。还有另一种解决方案:jsdom。一个非常棒的包,用它自己的话说:这是在 NodeJS 中实现的 DOM。

它确实比较好用,可以运行在 Node 环境下。使用 JSDom,你可以不把 DOM 当做 I/O 操作。这一点非常重要,因为要把 DOM 操作从前端代码中分离出来非常困难(实际工作中几乎不可能完全分离)。我猜 JSDom 的诞生就是因为这个原因:使得在 Node 中也可以运行前端测试。

我们来看一下它的工作原理,和往常一样,需要有初始化代码和测试代码。这次我们先看测试代码。不过正式看代码之前请先接受我的歉意。

4.了解React官方测试工具库

react测试可以分为测试DOM结构 和测试Action和Reducer

React官方测试工具库提供两种测试形式:

1.Shallow Rendering 测试虚拟DOM的方法 

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

import TestUtils from 'react-addons-test-utils';

function shallowRender(Component) {

  const renderer = TestUtils.createRenderer();

  renderer.render(<Component/>);

  return renderer.getRenderOutput();

}

Shallow Rendering 函数,该函数返回的就是一个浅渲染的虚拟DOM对象。只有一层,不返回子组件。

2.DOM Rendering 测试真实DOM的方法

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

import TestUtils from 'react-addons-test-utils';

import App from '../app/components/App';

const app = TestUtils.renderIntoDocument(<App/>);

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

 

Enzyme库对官方测试库进行了封装,它提供三种方法:

import { shallow, mount, render } from ‘enzyme’

shallow 返回组件的浅渲染,对官方shallow rendering 进行封装

const wrapper = shallow(<Counter {...props} />)

expect(wrapper.find('button').exists()).toBeTruthy()

shallow 返回Counter 的浅渲染,然后调用find 方法查找 button 元素

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

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

const wrapper = render(<Counter {...props} />)

expect(wrapper.find('button').exists()).toBeTruthy()

render方法与shallow方法的API基本是一致的。

Enzyme的设计就是,让不同的底层处理引擎,都具有同样的API

 

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

const wrapper = mount(<Counter arithmetic={arithmetic})

 wrapper.find('button').simulate('click')

 

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

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

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

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

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

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

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

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

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

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

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

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

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

 

toMatchSnapshot方法会去帮你对比这次将要生成的结构与上次的区别

 

测试 异步action

他的I/O可能依赖store.getState(),自身又会依赖异步中间件,这类使用原生js测试起来比较困难,我们的目的可以设定为:当我们触发一个action后,它经历了一个圈异步最终store.getAction中的action拿到的数据和我们预期一致。因此我们需要用到两个库:redux-mock-store 和 nock。

const mockStore = configureStore([thunk, promiseMiddleware]) //配置mock 的store,让他们有相同的middleware

afterEach(() => 

 nock.cleanAll()

)  //每执行完一个测试后,清空nock

 

const store = mockStore({

   router: {

      location: '/',

    },

})  //以我们约定的初始state创建store,控制 I/O 依赖

 

const data = [

//接口返回的信息

{…

},

]

 

nock(API_HOST) //拦截请求返回的response

.get(`/api/…`) //拼接路由,需要在test.js中配置测试路径

.reply(200, {code: 0, data})

return store.dispatch(actions.getAll()).then(() => expect(store.getActions()).toMatchSnapShot())

 

1.用nock来mock拦截http请求结果,并返回我们给定的response

2.用redux-mock-store 来mock store 的生命周期,需要预先把middleware配成和项目一致

3.describe会包含一些生命周期的api,比如全部测试开始做啥,单个测试结束做啥api,这里每执行完一个测试就清空nock

4.用了jest中的toMatchSnapShot api 来判断两个条件是否一致。

本来你要写成 expect(store.getActions()).toEqual({data …}) 你需要把equal 里的东西都描写具体,而toMatchSnapshot

可在当前目录下生成一个snapshot ,专门存放当前结果,写测试时看一眼是预期的就commit。如果改坏了,函数就不匹配snapshot了。

歉意

这一部分是这个测试系列文章中唯一使用指定框架的部分,这部分使用的框架是 React。选择 React 并不是因为它是最好的框架,我坚定地认为没有所谓最好的框架,我甚至认为对于指定的场景也没有最好的框架。我相信的是对于个人来讲,只有最合适,用着最顺手的框架。

而我使用着最顺手的框架就是 React,所以接下来的代码都是 React 代码。但是这里依然说明一下,前端集成测试的 jsdom 解决方案可以适用于所有的主流框架。

ok,现在回到正题。

5.测试注意事项

1.拆分单元,关注输入输出,忽略中间过程。dom测试时只用确保正确调用了action函数,传参正确,而不用关注函数调用结果,置于action处理结果,reducer中对state的改变这些都留给action和reducer自己的单元测试区测。不要想着测试整个大功能的流程,不要有闭环的思想,单元测试需要保证的当前单元正常,对于每个单元模块输入输出都正确,理论串联后一起使用闭环时也会正确。

 

2.多种情况的测试覆盖,如果不能保证测试的全面性,每种情况都覆盖到,那么这个测试就是个不敢依靠的不全面的测试。当然在实际项目中,可能因为时间、资源等问题,无法保证每种情况都测试到,而只测试主要的内容,这时候要做到心里有数,反正我是对于每个测试都写注释的,交代清楚测试覆盖了哪些,还有哪些没有覆盖,需要其他手段保持稳定性。

 

3.关注该关注的,无关紧要的mock掉。css、图片这种mock掉,http请求mock掉

 

4.原本不利于测试的代码还是需要修改的,并不能为了原代码稳定不变,在测试时不敢动原代码。譬如函数不纯,没有返回值等。

 

使用 Jsdom

JavaScript

const React = require('react') const e = React.createElement const ReactDom = require('react-dom') const CalculatorApp = require('../../lib/calculator-app') ... describe('calculator app component', function () { ... it('should work', function () { ReactDom.render(e(CalculatorApp), document.getElementById('container')) const displayElement = document.querySelector('.display') expect(displayElement.textContent).to.equal('0')

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const React = require('react')
const e = React.createElement
const ReactDom = require('react-dom')
const CalculatorApp = require('../../lib/calculator-app')
    ...
describe('calculator app component', function () {
        ...
    it('should work', function () {
        ReactDom.render(e(CalculatorApp), document.getElementById('container'))
        const displayElement = document.querySelector('.display')
        expect(displayElement.textContent).to.equal('0')

注意看第 10 – 14 行,首先 render 了 CalculatorApp 组件,这个操作同时也 render 了 DisplayKeypad。第 12 和 14 行测试了 DOM 中计算器的显示是否是 0(初始化状态下)。

上面的代码是可以运行在 Node 下的,注意到里面用的是 document。我第一次使用它的时候特别惊讶。全局变量 document 是一个浏览器变量,竟然可以使用在 NodeJS 中。在这简单的几行代码背后有着大量的代码支撑着,这些 jsdom 代码几乎是完美地实现了浏览器的功能。所以这里我要感谢 Domenic Denicola, Elijah Insua 和为这个工具包做过贡献的人们。

图片 1

第 10 行中也使用了 document(调用 ReactDom 来渲染组件),在 ReactDom 经常会使用它。那么在哪里创建的这些全局变量呢?在测试中创建的,见下面代码:

JavaScript

before(function () { global.document = jsdom(`<!doctype html><html><body><div id="container"/></div></body></html>`) global.window = document.defaultView }) after(function () { delete global.window delete global.document })

1
2
3
4
5
6
7
8
9
before(function () {
        global.document = jsdom(`<!doctype html><html><body><div id="container"/></div></body></html>`)
        global.window = document.defaultView
      })
 
    after(function () {
        delete global.window
        delete global.document
      })

代码中创建了一个简单的 document,把我们的组件挂在一个简易 div 上。同时还创建了一个 window,其实我们并不需要它,但是 React 需要。最后在 after 中清理全局变量。

documentwindow 一定要设置成全局的吗?滥用全局变量不论理论和实践的角度都不是个好习惯。如果它们是全局的,那这个集成测试就不能和其他的集成测试并行运行(这里对 ava 的用户表示抱歉),因为它们会互相覆写全局变量,导致结果错误。

然而,它们必须要设置成全局的,React 和 ReactDOM 要求 documentwindow 是全局的,不接受把他们以参数的形式传递。或许等 React fiber 出来就可以了?也许吧,不过现在我们还必须要把 documentwindow 设置成全局的。

事件处理

剩下的测试代码怎么写呢,看下面代码:

JavaScript

ReactDom.render(e(CalculatorApp), document.getElementById('container')) const displayElement = document.querySelector('.display') expect(displayElement.textContent).to.equal('0') const digit4Element = document.querySelector('.digit-4') const digit2Element = document.querySelector('.digit-2') const operatorMultiply = document.querySelector('.operator-multiply') const operatorEquals = document.querySelector('.operator-equals') digit4Element.click() digit2Element.click() operatorMultiply.click() digit2Element.click() operatorEquals.click() expect(displayElement.textContent).to.equal('84')

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ReactDom.render(e(CalculatorApp), document.getElementById('container'))
const displayElement = document.querySelector('.display')
expect(displayElement.textContent).to.equal('0')
const digit4Element = document.querySelector('.digit-4')
const digit2Element = document.querySelector('.digit-2')
const operatorMultiply = document.querySelector('.operator-multiply')
const operatorEquals = document.querySelector('.operator-equals')
digit4Element.click()
digit2Element.click()
operatorMultiply.click()
digit2Element.click()
operatorEquals.click()
expect(displayElement.textContent).to.equal('84')

测试中主要实现的是用户点击 “42 * 2 = ”,结果应该是输出 “84”。这里获取 element 使用的是广为人知的 querySelector 函数,然后调用 click 点击。还可以创建事件,然后手动调度,见下面代码:

JavaScript

var ev = new Event("keyup", ...); document.dispatchEvent(ev);

1
2
var ev = new Event("keyup", ...);
document.dispatchEvent(ev);

这里有内置的 click 函数,所以我们直接使用就好了。就是这么简单!

机智的你可能已经发现了,这个测试和前面的端到端测试其实是一样的。但是注意这个测试要快 10 倍以上,并且实际上它是同步的,代码也更容易写,可读性也更好。

但是如果都一样的话,那需要继承测试干嘛?因为这是个示例项目嘛,并不是实际项目。这个项目里面只有两个组件,所以端到端测试和继承测试是一样的。如果是在实际项目中,端到端测试可能包含了上百个单元,而继承测试只包含少量单元,比如包含 10 个单元。所以实际项目中只有几个端到端测试,而可能包含了上百个继承测试。

总结

本文中主要介绍了什么:

  • 介绍了使用 jsdom 方便地创建全局变量 documentwindow
  • 介绍了如何使用 jsdom 测试应用;
  • 介绍了,测试就是这么简单^_^。

    1 赞 收藏 评论

图片 2

本文由澳门威斯尼人平台登录发布于Web前端,转载请注明出处:测试环境,集成测试

相关阅读