利用Jest编写测试代码

前言

本想打算研究swagger2Mock的实现逻辑,琢磨了下代码之后发现,基本上是从一个格式转化到另一个格式,更多的是业务逻辑上的转换,没有太多的技术知识.不过在研究swagger-parser-mock这个功能库时,意外的收获到一些关于单元测试的编写实践.于是做下分享与记录.TL;DR

文档索引

  1. 特性
  2. 语法
  3. 工具函数的单元测试
  4. 结合开发场景,我们该怎么写工具类函数的单元测试
  5. 接口测试
  6. 结合开发场景,我们该怎么写接口单元测试
  7. 总结

知识点

特性

适应性:Jest是模块化、可扩展和可配置的。也就是说能应付小功能也可以应对大场景下的测试.

快速和沙盒:Jest虚拟化JavaScript环境,能模拟浏览器,并在工作进程之间并行运行测试。

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

快速交互模式: 错误信息会有帮助的颜色编码标记,堆栈跟踪快速指向问题的根源。

语法

Jest是Facebook开发的一个对javascript进行单元测试的工具,之前仅在其内部使用,后开源,并且是在Jasmine测试框架上演变开发而来,使用了我们熟知的expect(value).toBe(other)这种断言格式。这种格式符合我们的日常口语逻辑:期望某个值是某个值.

举个例子:

test('1 + 2 等于 3', ()=> {
    expect(1 + 2).toBe(3);
});

上面这段代码很简单,也很没有实用性,别慌!我只是为了引出test的使用方法.test实际上就是声明一个测试用例,第一个参数为用例的名称,想取啥取啥,不过一定要与测试代码的功能相关,不然执行测试脚本的时候,你就不能直观的看出来是哪段用例报错了.

而在test之上,又有叫做describe的东西.它是用例的集合,比如一段增删改查的流程就可以放在一个describe中,他的使用方法与test一致,都是首个参数为名称,第二个参数为测试执行函数.

其实要介绍的基本就这么多,接下来就是结合easyMock项目,分析几个具有代表性的例子.

案例分析

工具函数的单元测试

'use strict'

const util = require('../../util')
describe('test/util/index.test.js', () => {
  test('params', () => {
    let params = util.params('/api/:user/:id', '/api/souche/123')
    expect(params).toEqual({
      user: 'souche',
      id: '123'
    })

    params = util.params('/api/:user/:id', '/api/a%AFc/123')

    expect(params).toEqual({
      user: 'a%AFc',
      id: '123'
    })

    params = util.params('/api/:user/:id', '/api/123')

    expect(params).toEqual({})
  })
  //...
})

这段代码是为了测试util模块中封装的params方法,光看测试代码的编写,应该就能猜的出这个方法是用来干嘛的,没错!他主要是能根据一个格式化Url和一个实际Url生成实际url所表明的参数对象,而上面三段断言语句分别解决不同实际url的场景下,params能否返回期望的值.

从中可以学习到的就是对于工具型的函数,单元测试要尽可能的覆盖不同场景.每个场景一个测试用例,做到本方法的最大覆盖率.

结合一下开发中的应用场景,假设我们项目中用到如下一个对于数值的操作函数:

// test1.js
/**
 * 格式化数字默认保留1位小数,重点是如果最后是0则清掉
 * @param    {[type]}                 num 你的数字
 * @param    {Number}                 len 某认为1,想保留几个就保留几个
 * @return   {[type]}                     [description]
 *
 * @Author   zy
 * @DateTime 2017-11-03T18:24:59+0800
 */
let numToFixed = (num, len = 1) => {

    if (Math.floor(num) == num) {
        return num
    }
    return parseFloat(num.toFixed(len).replace(/0+$/, ''));
}
export {numToFixed}

结合开发场景,我们该怎么写工具类函数的单元测试

结合下尽可能出现的场景,试着写下单元测试:

import {numToFixed} from '../test1'

describe('numToFixed', () => {
  test('参数为整数', () => {
    let res = numToFixed(2)
    expect(res).toBe(2)
  })
  test('参数为两位小数', () => {
    let res = numToFixed(2.16)
    expect(res).toBe(2.2)
  })
  test('参数为两位小数,保留两位小数', () => {
    let res = numToFixed(2.16, 2)
    expect(res).toBe(2.16)
  })
  test('参数为两位小数,保留两位小数,并且结果的最后一位是零', () => {
    let res = numToFixed(2.101, 2)
    expect(res).toBe(2.1)
  })
  test('参数为两位小数,保留两位小数,并且结果的最后一位会因为进位加一', () => {
    let res = numToFixed(2.107, 2)
    expect(res).toBe(2.11)
  })
})

执行jest命令,得到如下测试结果:

➜  controllers git:(master) ✗ jest fixed.test.js
 PASS  ./fixed.test.js

Test Suites: 1 passed, 1 total
Tests:       5 passed, 5 total
Snapshots:   0 total
Time:        0.943s, estimated 1s
Ran all test suites matching /fixed.test.js/i.

然后实际开发的时候,有同事发现这个操作函数最后的replace方法好像可以去掉,于是去掉代码之后,我们再次跑一遍单元测试命令:

➜  controllers git:(master) ✗ jest fixed.test.js
 PASS  ./fixed.test.js

Test Suites: 1 passed, 1 total
Tests:       5 passed, 5 total
Snapshots:   0 total
Time:        0.314s, estimated 1s
Ran all test suites matching /fixed.test.js/i.

竟然神奇的发现测试的总体时间减少了三分之一,并且全部通过了测试.当然,不排除一些机子运行状态不同导致的时间差别,不过能少写段代码自然是最好的.

接口测试

稍微看下easy-mock的单测骨架:

describe('test/controllers/group.test.js', () => {
  let request, user, soucheUser
  afterAll(() => spt.cleanCollections())
  beforeAll(async () => {
    user = await spt.createUser()
    request = spt.createRequest(app.listen(), user.token)
  })
  describe('create', () => {
    test('参数验证', async () => {
      const res = await request('/api/group/create', 'post')
      expect(res.body.message).toBe('params error')
    })
    //..test
  })
  //...describe
})

先来聊聊几个生面孔:

用例说明: 看描述就知道,这是个测试参数验证的用例,希望发送一个不带参数或者带错参数的情况下,可以抛出预期的错误文案或者错误处理.光看代码,应该也可以很清楚的看出来了.

结合开发场景,我们该怎么写接口单元测试

EasyMock不同的是,我们的后端逻辑由C#实现,无法保证前端请求接口时有个临时的数据库映射供我们测试,所以要写接口单元测试的话意义不大,仍然会对数据库产生数据,跟现有的后端做的伪单元测试类似,还需要将测试数据删除.不过还是稍微写个demo意思下:

const request = require('supertest');
const url = 'http://192.168.5.187:15844';
describe('login', () => {
	test('正常登录', () => {
		request(url)
			.post('/api/MemberShip/Login')
			.send({
				name: 'admin',
				password: '123456'
			})
			.then(data => {
				expect(JSON.parse(data.text).state).toBe(0)
			})
	})
	test('密码错误', () => {
		request(url)
			.post('/api/MemberShip/Login')
			.send({
				name: 'admin',
				password: '666'
			})
			.then(data => 
				expect(JSON.parse(data.text).state).toBe(1010013)
			})
	})
})

本代码主要是写了个登录接口的测试,然后用例分为密码错误和正常情况两种用例,当密码不正确的时候,我们期望可以抛出对应的错误码,于是执行一遍单元测试:

➜  role_tools git:(local1.5.5) ✗ jest
 PASS  ../../../test/api/user.test.js
  login
    ✓ 正常登录 (9ms)
    ✓ 密码错误 (1ms)

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        0.778s, estimated 1s
Ran all test suites.

恭喜,接口测试成功!(戏精上身..)

总结

由于easyMock项目中的单元测试,基本上就涉及以上两种测试类型,具体还有其他什么类型的测试还需要在实际项目中实行一段时间才知道,对于工具类的单元测试,个人还是认为有必要在前端进行测试覆盖的,这样对于代码的质量保证具有一定意义.而接口测试的话,由于业务逻辑不是通过node写,jest覆盖不到实际的业务逻辑,意义不是很大,而且成本也大,毕竟每个接口还要考虑不同的场景,效益原低于成本,所以对于我们目前的项目结构的话,接口测试个人认为可以不必要.