测试的讨论与思考
为什么要写测试
在一个项目当中编写测试代码的时间远比编写代码时间要长,并且更多的测试代码代表着更多的bug出现,目前而言,测试代码的组织和维护缺乏工具和框架,手动维护成本高,且基于测试的思路容易走向系统的过度设计,关注点脱离了主流,那么这么多问题我们为何还要写测试代码呢?
测试的道理
简单的来说写代码比作开赛车一样,测试就像个防撞带,对于那些高手,大师是不需要的,而对于刚入门的新手,防撞带可以防止新手车毁人亡。
如何正确的编写测试代码
- 首先测试的价值毋庸置疑的,但是我们要分清界线。哪些代码需要测试,正常来说不是所有代码都需要注释,也不是所有代码需要测试,核心的业务,核心的逻辑,有重构价值的代码,有开放共建的代码,这个一部分代码是有添加单测的必要的。
- TDD只适合重构自己的代码的场景,不要盲目使用。想TDD,BDD这些专有名词看着这么牛的东西,很容易勾起实践的欲望,但是这东西在没有对系统明确的了解,是无法使用的,对于国内环境来说需求分析结束后,马上就需要你去书写代码,那么根本无法在写代码前对项目解读,对各个环节进行解读,分析。
- 不要着急实现测试的细节,只关注输入输出,开始写测试代码,我们还是主要已代码输入和输出判断他是否符合要求。
- 挑选适合的测试框架,避免做无意义的“编程卡顿”。选择一个好的适合当前项目的框架,不至于在开发一半时遇到因为测试不能按理想通过的情况,而花费大量时间去做这一部分问题。
- 添加测试的主要目的就是为了让你写高质量代码,不要因为写了测试用例后发现项目冗余,脱离初衷。
如何最佳的实践测试
- 测试代码必须要快速执行,避免花大量时间去等待结果。
- 不会依赖环境和顺序,有自己的3A环节
- 测试代码执行一次还是无数次,你的结果应该是一样,也就是说你写的测试是不能改变持久化的数据。
- 测试要给出结果,是否通过,要正确的断言,判断情况。
基准测试和压力测试
关注服务的性能
我们该如何去观察和测试一个服务的性能如何呢
- 使用benchmark对核心代码进行基准测试
- 根据服务业务场景和经验估算业务访问量PV
- 使用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"
运行出来大概就是
codding(node)
搭建测试环境
- 选择合适的框架,在ava/mocha/jasmine/tap等框架中选择趁手的武器,这里我选择mocha。
- 使用supertest去作为客户端代理,替我们向服务发送请求。
- 引入断言库去辅佐mocha编写单测。
- 在钩子中初始化服务器环境,重置数据库数据,并且还要处理掉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()
})
})
})
这里处理常见的增删改查接口进行了测试。
最后运行覆盖测试的脚本,查看覆盖率。
总结
为什么要编写测试代码
其主要也是最大的原因,让新手撞死在这些代码上,同时如果有一套完善的测试,些许会提高代码的维护性,通过测试结果,更快定位问题。
什么时候要写测试,什么样的代码要写测试
不是所有代码都需要注释,也不是所有代码需要测试,核心的业务,核心的逻辑,有重构价值的代码,有开放共建的代码,这个一部分代码是有添加单测的必要的。