node写RESTful接口后对单元测试思考--为什么要写测试,该如何去正确书写测试。

110 阅读7分钟

测试的讨论与思考

为什么要写测试

在一个项目当中编写测试代码的时间远比编写代码时间要长,并且更多的测试代码代表着更多的bug出现,目前而言,测试代码的组织和维护缺乏工具和框架,手动维护成本高,且基于测试的思路容易走向系统的过度设计,关注点脱离了主流,那么这么多问题我们为何还要写测试代码呢?

测试的道理

简单的来说写代码比作开赛车一样,测试就像个防撞带,对于那些高手,大师是不需要的,而对于刚入门的新手,防撞带可以防止新手车毁人亡。

如何正确的编写测试代码

  • 首先测试的价值毋庸置疑的,但是我们要分清界线。哪些代码需要测试,正常来说不是所有代码都需要注释,也不是所有代码需要测试,核心的业务,核心的逻辑,有重构价值的代码,有开放共建的代码,这个一部分代码是有添加单测的必要的。
  • TDD只适合重构自己的代码的场景,不要盲目使用。想TDD,BDD这些专有名词看着这么牛的东西,很容易勾起实践的欲望,但是这东西在没有对系统明确的了解,是无法使用的,对于国内环境来说需求分析结束后,马上就需要你去书写代码,那么根本无法在写代码前对项目解读,对各个环节进行解读,分析。
  • 不要着急实现测试的细节,只关注输入输出,开始写测试代码,我们还是主要已代码输入和输出判断他是否符合要求。
  • 挑选适合的测试框架,避免做无意义的“编程卡顿”。选择一个好的适合当前项目的框架,不至于在开发一半时遇到因为测试不能按理想通过的情况,而花费大量时间去做这一部分问题。
  • 添加测试的主要目的就是为了让你写高质量代码,不要因为写了测试用例后发现项目冗余,脱离初衷。

如何最佳的实践测试

  1. 测试代码必须要快速执行,避免花大量时间去等待结果。
  2. 不会依赖环境和顺序,有自己的3A环节
  3. 测试代码执行一次还是无数次,你的结果应该是一样,也就是说你写的测试是不能改变持久化的数据。
  4. 测试要给出结果,是否通过,要正确的断言,判断情况。

基准测试和压力测试

关注服务的性能

我们该如何去观察和测试一个服务的性能如何呢

  1. 使用benchmark对核心代码进行基准测试
  2. 根据服务业务场景和经验估算业务访问量PV
  3. 使用ab对服务吞吐率、响应时间。并发数进行测试
    • 响应时间: 系统对请求进行处理并做出响应的时间
    • 吞吐量:系统在单位时间内处理请求的数量,响应时间的倒数。
    • 并发用户数:系统可以同时承载的正常使用系统功能的用户数量。

如何预估服务性能和所需资源

  • 单机QPS = 总请求数/(进程总数*请求时间),数据需要从压测得到,然后计算
  • 峰值时间QPS=(总PV80%)/(每天秒数20%)= pv(W)/2.16
  • 单台机器的总pv = QPS36008
  • 需要机器 = 峰值时间QPS/单机QPS

计算

假设每天300w pv在单台机器上,这台机器的QPS是多少?

(300000080)/(864000.2) = 139QPS 假设一台机器的QPS是58,需要几台机器来支持呢? 139/58 应该等于二点几,但是为了考虑到一定要比这个数据大,所以应该请求3台机器。

覆盖测试

对于这个,个人保持中立态度,因为,不是所有的程序都要测试,而是要对于这个方面择情去做测试,有时可能最近工期特别忙,那么就先为了项目的运行为主,而不用太过于关注测试,而相对之后空闲可以补测试用例,而有时的项目内容其实我个人认为是不用做测试的,而核心的业务,核心的逻辑,有重构价值的代码,有开放共建的代码,这个一部分代码是有添加单测的必要的。

// 这里使用nyc 对 mocha进行了覆盖率测试 package.json
"cov":"nyc mocha 'test/**/*.test.js' --exit"

运行出来大概就是

image.png

codding(node)

搭建测试环境

  1. 选择合适的框架,在ava/mocha/jasmine/tap等框架中选择趁手的武器,这里我选择mocha。
  2. 使用supertest去作为客户端代理,替我们向服务发送请求。
  3. 引入断言库去辅佐mocha编写单测。
  4. 在钩子中初始化服务器环境,重置数据库数据,并且还要处理掉require缓存。
//重置数据库,出来了require缓存伪代码
beforeEach(function(){
delete require.cache[require.resolve('./server')]
server = require('./server')
})

实践

安装 -D 环境下 mocha power-assert nyc(覆盖测试的) supertest

yarn add -D mocha power-assert nyc supertest

修改 test脚本

+ "test": "mocha 'test/**/*.test.js' --exit", - "test": "echo "Error: no test specified" && exit 1",

创建目录,然后开始编写测试代码,最好你test文件的编写目录和主项目进行对应。

首先require 我们的工具

const app = require('../server')
const request = require('supertest')(app)
const assert = require('power-assert')

对一套规则进行测试断言

  • describe 描述一个组
  • it 编写某个具体的单元测试,第一个参数描述这个接口操作,第二个参数是一个回调,回调的参数的一个往下走的方法。
  • 通过supertest工具去测试请求
  • expect 期待请求的状态
  • end 对返回的参数进行后处理,并在里面断言数据,检查通过
  • power-assert 是对node的assert的一个封装,为了我们更方便的断言结果。
describe('# test routers', function () {
	// 定义公共请求数据/用于和
	const temp = {
		name: 'mocha-test',
		template: '<h2>Hello ${name}</h2>',
		data: '{name:"mocha"}',
	}
	// get
	it('GET /api/v1/email', (done) => {
		request
			.get('/api/v1/email')
			.expect(200)
			.end((err, res) => {
				if (err) return done(err)
				assert(res.body.code === 200)
				assert(res.body.msg === 'success')
				assert(Array.isArray(res.body.data), '返回数据应该是数组类型')
				done()
			})
	})
	// post
	it('POST /api/v1/email', (done) => {
		request
			.post('/api/v1/email', temp)
			.expect(200)
			.end((err, res) => {
				if (err) return done(err)
				assert(res.body.code === 200)
				assert(res.body.msg === 'success')
				assert(
					typeof res.body.data === 'object',
					'返回数据应该是对象类型'
				)
				assert(res.body.data._id !== undefined, '应返回新增元素数据')

				done()
			})
	})
	// get ID
	it('GET /api/v1/email/:id', (done) => {
		request
			.get('/api/v1/email/6372edcaddf961bd599c4286')
			.expect(200)
			.end((err, res) => {
				if (err) return done(err)
				assert(res.body.code === 200)
				assert(res.body.msg === 'success')
				assert(
					typeof res.body.data === 'object',
					'返回数据应该是对象类型'
				)
				assert(res.body.data.name === 'test', '返回数据应和数据库一致')
				done()
			})
	})
	// bad get ID
	it('GET /api/v1/email/:id  bad Id', (done) => {
		request
			.get('/api/v1/email/6372edcaddf961bd599c4281')
			.expect(200)
			.end((err, res) => {
				if (err) return done(err)
				assert(res.body.code === 400)
				assert(res.body.msg === 'success')
				assert(
					typeof res.body.data === 'object',
					'返回数据应该是对象类型'
				)
				done()
			})
	})
	// put
	it('PUT /api/v1/email/:id', (done) => {
		request
			.put('/api/v1/email/6372edcaddf961bd599c4286', temp)
			.expect(200)
			.end((err, res) => {
				if (err) return done(err)
				assert(res.body.code === 200)
				assert(res.body.msg === 'success')
				assert(
					typeof res.body.data === 'object',
					'返回数据应该是对象类型'
				)
				assert(res.body.data.name === 'test', '返回数据应和入参一致')
				done()
			})
	})
	// delete
	it('DELETE /api/v1/email/:id', (done) => {
		request
			.delete('/api/v1/email/63720f901eaf0a808c410a1c')
			.expect(200)
			.end((err, res) => {
				if (err) return done(err)
				assert(res.body.code === 200)
				assert(res.body.msg === '删除成功')
				done()
			})
	})
})

这里处理常见的增删改查接口进行了测试。

最后运行覆盖测试的脚本,查看覆盖率。

总结

为什么要编写测试代码

其主要也是最大的原因,让新手撞死在这些代码上,同时如果有一套完善的测试,些许会提高代码的维护性,通过测试结果,更快定位问题。

什么时候要写测试,什么样的代码要写测试

不是所有代码都需要注释,也不是所有代码需要测试,核心的业务,核心的逻辑,有重构价值的代码,有开放共建的代码,这个一部分代码是有添加单测的必要的。