单元测试整理

2,178 阅读6分钟

学习单元测试的时候接触了很多概念karma、mocha、Jesmine、chai、expect、assert、should、sinon等,容易混乱,在此做个梳理。

1. 测试框架 Mocha、Jesmine

1.1 Mocha

入门文章参考:www.ruanyifeng.com/blog/2015/1…

Mocha是一个常用的JS测试框架,可以在浏览器和Nodejs环境使用。Mocha不带断言需要和断言库结合使用。项目中使用的也是Mocha+chai+sinon的结合。例如给ndfront组件写的单元测试,详情查看github仓库:github.com/baihexx/ndf…

特点:灵活,可扩展性好,可配合不同的断言库使用,但是自身集成度不高

1.2 Jesmine

Jesmine也是常用的测试框架,项目中没有用这个。

特点:内置断言库,集成度高,方便支持异步测试,但是灵活性差,断言风格单一

2. 断言库

2.1 assert

assert模块是Node的内置模块,用于断言。
官方API
常用API

eg:

var assert = requier('assert')
describe('desc1', function() {
    it('desc2', function() {
        assert(a === 1, '预期a的值是1')
    })
})

2.2 should.js

github 仓库
API docs

should.js是个第三方断言库,常和Mocha联合使用。

  • 使用方法1:
    requier('should'): 扩展Object.prototype,增加should属性,所有Object可以直接获取should使用,eg:
var should = require('should');
(5).should.be.exactly(5).and.be.a.Number();

var a = null
a.should.not.be.ok() // 报错
  • 使用方法2:
    若是undefined或者null,并没有继承Object的原型链,没有should属性可用,可采用如下方法:
var should = require('should/as-function');
var a = null
should(a).not.be.ok() // pass
should(10).be.exactly(5).and.be.a.Number();

2.3 Chai

官网API: 安装方法等查看官网

Chai是个断言库,常和Mocha结合使用。他有多种断言风格(assertion style):assert, expect, should

  • assert断言风格:和nodejs的assert模块类似(多了写语法糖),是一种非链式语言风格 eg:
var assert = requier('chai').assert
assert.notEqual(3, 4, 'these numbers are not equal')
  • expect断言风格:expect和should都是BDD风格,是一种链式语言风格, 连接词有 to,be,been,is等自然语言, eg:
var expect = requier('Chai').expect
expect([1, 2, 3]).to.be.an('array').that.includes(2)
  • should断言风格:should()扩展了Object.prototype,增加了should属性,使用方法如下。eg:
var should = require('chai').should() //actually call the function
var foo = 'bar'
foo.should.be.a('string')
foo.should.equal('bar')
foo.should.have.lengthOf(3)

(注意should对IE兼容性不好)

3. 测试运行工具:karma

Github 仓库

定义:A simple tool that allows you to execute JavaScript code in multiple real browsers. The main purpose of Karma is to make your test-driven development easy, fast, and fun.

Karma不是测试框架,也不是断言库,他会开启一个HTTP服务,将测试文件生成一个Html文件,在浏览器内运行、调试。Karma不指定测试框架,通过插件和Mocha、Jesmine、QUnit都可以结合使用。

配置项较多,根据官网说明配置,并不难。

测试覆盖率:根据提示安装、配置即可生成覆盖率报告

4. 测试辅助工具:Sinon

Github 仓库
官网
入门参考(侵删)

为什么需要Sinon?在做单元测试的时候,我们会发现我们要测试的方法会引用很多外部依赖的对象,比如:(发送邮件,网络通讯,记录Log, 文件系统之类的),而我们没法控制这些外部依赖的对象。例如:前端项目通常是用Ajax去服务端请求数据,得到数据之后做进一步的处理。但是做单元测试的时候通常不真的去服务端请求数据,不仅麻烦,可能服务端接口还没做好,这种不确定的依赖使得测试变得复杂。所以我们需要模拟这个请求数据的过程,Sinon用来解决这个问题。

Sinon的工作本质是“测试替身”,测试替身用来替换测试中的部分代码,使得测试复杂代码变得简单。

Sinon提供了三个功能:示例讲解看入门文章,不再赘述

  • spy(间谍):提供函数调用的信息,但不会改变函数的行为
  • stub:与spies类似,但是会完全替换目标函数。这使得一个被stubbed的函数可以做任何你想要的 —— 例如抛出一个异常,返回某个特定值等等。
  • mock:通过组合spies和stubs,使替换一个完整对象更容易

eg:admin/misc/user.js: user.getUser() (看不懂的随便看看,这是实际的项目代码单元测试)
user.getUser函数用来获取用户信息,用户输入工号后向服务端请求数据,我们的测试用例重点在前端代码,不应依赖服务端才可测试,so 应该模拟ajax请求。
(1)nd-spa中ajax.js请求代码如下:实际的请求函数为:request.get, so应该mock request的get函数

(2)测试用例代码如下:

  • sinon.stub(obj, functionname, mockFun)
  • ajax.js中的请求代码调用的set(),send(),end()函数实际是mock中定义的函数,并没有做实际的后端请求,end的callback直接返回数据
  • 注意L11:设置函数最大时长,写成function(done)形式才可用this.timeout,否则es6的箭头函数中this是window

5. 多步骤测试用例(实际项目记录,可不看,估计看不明白)

用户在前端页面中通常是通过点击鼠标期待某种效果,这个过程通常不做单元测试,因为复杂度较高,且页面变动较快,性价比很低。但是项目中某些公共的业务组件,需求变动小,步骤相对简单,但使用又非常多,例如social管理后台中的搜索用户admin/misc/user.js:user.autoComplete(),完整的过程是:用户输入用户信息,然后选择搜索到的匹配用户,再点击搜索到的用户,该输入框的值变为选择的用户。这个过程如何编写单元测试。
这里涉及到多步骤的模拟。

(1)util.js 封装多步执行函数

// utils.js
export function triggerHTMLEvents (target, event, process) {
  const e = document.createEvent('HTMLEvents')
  e.initEvent(event, true, true)
  if (process) process(e)
  target.dispatchEvent(e)
  return e
}

export function triggerMouseEvents (target, event, process) {
  const e = document.createEvent('MouseEvents')
  e.initEvent(event, true, true)
  if (process) process(e)
  target.dispatchEvent(e)
  return e
}

export function triggerUIEvents (target, event, process) {
  const e = document.createEvent('UIEvents')
  e.initEvent(event, true, true)
  if (process) process(e)
  target.dispatchEvent(e)
  return e
}

// timeout: 执行间隔可设置
function createStep ({ step, timeout }) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      try {
        resolve(step())
      } catch (err) {
        reject(err)
      }
    }, timeout || 0)
  })
}
// 多步骤执行函数, 执行步骤封装在arr数组中
export function runSteps (arr) {
  if (arr.length === 0) {
    return
  }
  let firstStep = createStep(arr[0])
  const others = arr.splice(1)
  others.forEach(item => {
    firstStep = firstStep.then(() => {
      return createStep(item)
    })
  })
}

(2)使用

L116:给input设置值
L117:触发input的change事件,执行nd-autocomplete/src/input.js中change事件,如何调用的看autocomplete组件 L122:点击搜索列表的item,将值设置到input,然后验证input的值

过程中遇到一个额外的问题,在此记录下,备忘
(1)user中getUsers函数用到ucOrgId,查看代码(var ucOrgId = auth.getAuth('uc_org_id') 且仅仅在登录代码中有auth.setAuth())可知该组织id信息必须有,但是测试页面中没有登录,获取不到该信息,so,造登录数据,并设置到auth中。

(2)造登录数据并设置到auth中时遇到一个问题:若在user.spec.js中 引入auth,then auth.setAuth(...), 结果不对
原因:user.js中在开头就执行了获取ucOrgId的函数,我们在测试代码中先引入auth和user,这时ucOrgId已经获取了,且是空值,即使假造登录数据的函数写在import user之前也是没用,因为es6有提升的功能,总是先执行import,导致函数执行在后面,解决方法是:将设置登录数据的函数写在单独的文件中,使用import的方法,且在user之前inport,就会达到先执行设置登录数据的效果。(具体看代码更清晰)
(import的提升再复习下)

Jest TODO