正儿八经地写JavaScript之单元测试
前段时间阅读《Android编程权威指南》,第21章专门介绍了Android中的单元测试,当时照猫画虎进行了一点练习,感觉在项目中引入单元测试确实是一件事半功倍的事情。于是开始着手查询JavaScript单元测试,才发现原来已经有这么多工具了。下面是对单元测试与Mocha
使用心得的一些整理。
在谷歌下搜索javascript unit test
,推荐链接展示了一堆mocha
的相关搜索,发现原来是TJ
大神写的,作为一个脑残粉,于是就选择了mocha
。
参考:
单元测试
开发是一个增量的过程,拿最近的项目来说:
- 前期需要按照指定的规则(类似于一个模板引擎)在客户端解析用户上传的txt文本,提取作者,时间,概述等基本内容;
- 需求更新,要求在原来的解析规则上适当放开限制,提高代码的容错率
- 需求更新,运行用户上传docx文档,解析图片内容并自动上传到服务器,替换为对应的URL
总体来说这个需求是围绕着文本解析进行的。在最初开发的时候,根本不知道后面的需求迭代;而后续开发是在前面的功能基础上进行的,尤其是第二条,简单来讲就是增加可匹配的标签,但是必须提防的是增加的标签不会影响前面已经实现的功能~总之越往开发后期,代码改的越心惊胆颤。
要是早点学习了单元测试该多好!单元测试就是为了验证代码的功能是否会如预期般生效。
我的理解是:我们会在开发过程中手动去检测某个函数是否会返回正确的结果,某个分支是否会在指定条件下进入等,单元测试就是用来测试我们写的功能代码,更重要的是,测试代码是可以重复执行的,也就是说,我们可以在后续开发的过程中折回来测试先前的功能,需要的成本仅仅只是执行一个命令而已。
那么,测试框架又是什么呢?
测试框架的职责即提供一套 API 帮助开发者更方便的测试代码
简单来讲,测试框架可以更方便的帮我们保管(组织)测试代码,展示测试结果及测试覆盖率等;具体来讲,测试框架可以帮我们执行测试逻辑,包括异步测试,测试过程钩子函数,断言库等。
选择mocha的原因主要是:攻略太多了啊喂~
mocha使用
关于mocha的使用,官网上已经介绍的比较详细了,网上也能找到大量的教程。这里只是简单整理一下。
安装
只需要进行下面三步即可:
- 全局安装mocha,
mocha -h
可查看帮助命令 - 在项目目录下新增一个
test
文件夹,用于存放测试文件,一般命名为XX.test.js
这样 - 然后在与
test
同级的目录使用mocha
命令,会自动执行test
目录下的所有js文件(所以测试目录下就不要放其他无关的文件了)
API
describe和it
describe
方法用来描述和执行一组测试,第一个参数会输出到控制台,作为这组测试的标识符it
方法用来描述某个测试用例,同样会在控制台输出,如果测试用例通过,则会在其前面打勾
let assert = require("assert");
describe("demo", function () {
it("100% success", function(){
assert.equal(1, 1);
})
})
相关的文字描述可以用来组织层次化的测试用例,比如针对某个的对象的某些方法,为每个方法进行数个测试案例,则代码层次可以为
describe("demo", function () {
describe("foo", function () {
it("", function(){
// todo some assert
})
it("xxx", function(){
// some assert
})
})
describe("bar", function () {
it("xxx", function(){
// some assert
})
})
it("100% success", function(){
assert.equal(1, 1);
})
})
测试代码主要是为开发人员服务,因此语义化的代码尤其重要,为了更加直观的组织测试代码,mocha还额外提供了一个context
方法,实际上他只是describe
方法的别名而已,可以灵活使用。
钩子函数
在测试过程中,mocha提供了4个钩子函数
before
,在当前describe
块的所有测试用例之前调用,调用一次after
,在当前describe
块的所有测试用例之后调用,调用一次beforeEach
,在当前describe
块的每个测试用例调用之前都会调用,调用N次afterEach
,在当前describe
块的每个测试用例调用之后都会调用,调用N次
下面的代码简单测试一下
describe("hooks", function () {
var a = 0;
// 其余方法省略~
beforeEach(function () {
console.log(++a);
});
it("case 1", function(){
assert.equal(1, 1)
})
it("case 2", function(){
assert.equal(10, 10)
})
})
钩子函数在某些时刻非常有用,比如为了验证数据库模型的insert
方法,需要向数据库插入一条记录,在测试执行完毕,需要将对应的记录移除(测试数据不应当保留),对应的逻辑放在钩子函数里面会非常合适。
需要注意的是,如果不存在it
函数,那么也不会执行钩子函数哦~
done
JS中的异步代码是非常常见的,比如网络请求,数据库操作,定时器等,用mocha来测试异步代码也十分简单:使用,并在异步结束执行之后调用done即可完成测试用例的执行。
describe("demo", function () {
var a = 1;
beforeEach(function (done) {
setTimeout(function(){
a = 10;
done();
}, 1000)
});
it("asynchronous demo", function(){
assert.equal(a, 10)
})
})
文档上的done方法讲解的并不是很容易理解,这里参考
如果在describe
,it
及钩子函数的回调函数中传入了done
参数,则必须等待该done
完成调用之后才会执行后面的测试案例。
describe("done", function(done){
context("has done", function(data){
setTimeout(function(){
done();
}, 200);
it("done test1", function(){
console.log("it里的输出1");
});
it("done test2", function(){
console.log("it里的输出2");
});
console.log("it外的输出");
})
context("no done", function(){
console.log("no done()");
});
// 执行结果依次为 it外的输出->no done()->it里的输出1->it里的输出2
});
可见done会阻塞它所位于块内的后面的it
方法,我们还可以写一个通用的异步测试的方法
function promiseDescribe(describeName, task, assertFunc){
describe(describeName, function () {
let globalVal = {};
beforeEach(function (done) {
task(globalVal, done);
});
if (assertFunc instanceof Array) {
assertFunc.forEach(func => {
func(globalVal);
});
} else {
assertFunc(globalVal);
}
});
};
下面是使用方法
promiseDescribe("async", function(globalVal, done){
setTimeout(function(){
globalVal.a = 10;
done();
})
}, function(globalVal){
it("should", function(){
assert.equal(globalVal.a, 10)
})
})
由于文档还没有看全,之前异步代码测试我都是使用这个简陋的方法进行的。
描述符
单元测试可以在边开发边进行测试,最好的方式的完成一个功能就编写对应的测试用例,虽然会耽搁一些时间,但是与之后返回进行手动测试相比,这一切都是值得的,此外在回头写文档的时候也可以直接参照测试用例进行。但是问题来了,在开发的时候我们只希望执行某一些测试用例而非全部测试,mocha为我们提供了相关的描述符
it.only()
,只执行带有only
修饰的测试用例,如果同时存在多个带only的测试用例,则他们都会被执行(only貌似就有点名不副实了),如果不存在only修饰,则会执行所有的测试用例,所以在开发的时候only方法非常有用(可以立即测试我们刚写的接口)。it.skip()
,在某些时候测试用例需要进行调整或者跳过,则可以使用skip修饰,此时在控制台对应的测试用例会显示pending
;对于作废的测试用例,官方的建议是skip而不是直接删除掉
需要注意的是,不仅可以在某个测试用例上使用描述符,也可以为某个用例集合describe
或context
使用,在测试用例很多的情况下就会很有用哦~
断言
前面提到,单元测试主要是为了验证程序的功能是否正常,那么,功能的正常与否是如何判断的呢?大多数时候,单元测试都是针对接口和库函数进行的,而对于接口和函数的评定标准无非是传入对应的参数,是否返回预期的结果(正确的结果由我们手动给定)。因此,在单元测试中使用断言是在正常不过的了。
断言即我们相信程序在某种条件下必定会输出的某个确定的结果。断言库提供了方便的api来判断程序是否输出了指定的结果,上面代码中的assert
就是断言库。在官方文档中推荐了几种断言库
assert
,node内置断言库,功能比较少should.js
,听名字貌似很直观的样子chai
,上面的assert
使用的就是chai
,不过我也是刚接触,所以不是很熟悉,这里是文档
需要注意的是,如果书写合适的断言是单元测试中一个比较难的地方,下面提到测试用例的时候会再次说明。
测试用例
为了练习单元测试,我尝试着写了一个mysqljs模型类,然后为这个模型类编写测试用例。测试用例实际上就是单独调用某个方法,并测试其输出结果的一组逻辑代码。
在测试的过程中,发现了不少问题。下面这几个问题是我对自己的提问,到目前为止,并没有合适的解决答案~
如何组织测试用例
前面也提到了,测试代码主要是面向开发人员的,因此为了高效率的开发,如何组织测试用例就显得尤为重要。
mocha可以在describe
和context
中进行嵌套,这样就可以在控制台输出树状的测试结果,非常直观。此外it
描述语句要尽可能清晰的描述该测试用例,也就是“需要测试的单元在给定的条件和参数下会发生什么事情”。
此外还可以将测试用例按模块和功能以单文件的形式进行分类整理(而不是将所有的测试代码都放在同一个文件中),这点可以参考jQuery
源码的测试代码,在其test/unit
目录下,对各个模块如ajax
,animation
等测试用例就是按文件进行管理的。
举例来说,模型类一般会提供CRUD的接口,可以组织对增删查改四个测试用例集合;就查找来讲,又可以单独测试where
,order
,limit
等接口;拿where来讲,又需要测试默认where(id, 1)
和where(id, ">", 1)
这样的分支情形。总之,按层次来组织测试用例可以更清晰的帮助我们测试代码,避免遗漏某些地方,也是下面要提到的。
如何相信测试用例
如果测试代码本身有问题,那么测试不仅毫无意义,还会浪费更多的调试时间(调试源码,调试测试代码),因此,我们必须确保测试用例的正确性。关于这个问题,知乎上有一个回到:如何保证测试用例又少又准确?
精简
测试用例应当一眼就能看出其测试母的,尽量避免任何分支逻辑,因为我们需要的只是传入确定的参数,断言预期的结果而已,对于分支的测试应当另外写测试用例,而不是在同一个测试用例中增加逻辑操作。
考虑全面,逐步完善
测试用例需要尽可能考虑全面,避免遗漏某些分支;此外在接口的使用过程中如果出现了超出预期的结果,也可以回头补充测试用例。换句话说,测试也是一个逐渐完善的过程,我是在边开发边测试的,即完成一个接口就立即测试,遇见后面接口调整的时候(比如参数位置,输出结果格式化等),会回过头来重新调整测试用例。
如何写断言
断言就是断定程序在这个条件下必定会输出的结果,断言的正确与否直接影响测试用例是否通过,一个好的断言的结果应该是固定的,即测试用例本身没有问题的情况下,无论运行多少次,都会得到统一的结果:要么通过,要么失败。
这个问题在我写测试用例的时候十分纠结,因此并不能保证数据库的数据是不会变化的,也就是说即使现在id=1
的数据必定会返回root
,在将来的某个时候数据发生改变,则这个断言必定失败。这种“硬编码”的形式明显是很不合理的。
promiseDescribe("=", function (globalVal, done) {
admin.where("id", 1).select().then((res) => {
globalVal.name = res[0].name;
done();
});
}, function (globalVal) {
it("'id = 1' should return root account", function () {
assert.equal("root", globalVal.name);
});
});
也许使用钩子函数,在用例执行之前插入数据,然后再测试查询并将结果与插入的数据进行比较要更合理一些。但是这样测试用例就会加入大量的逻辑代码,测试的时间也会更长~
总之,如何写好断言不是一件简单的事情。
小结
由于单元测试这块也是刚接触,上面的整理会有理解错误和遗漏的地方,后面再回来填(这个梗我都用了无数次了,貌似很少有填坑成功的~)。不过单元测试对于开发而言确实十分有帮助,在开发完成之后跑一通测试,看见满屏幕的勾,成就感简直爆棚,哈哈。
磨刀不误砍柴工,不要嫌写测试用例麻烦。PS:今后也会尝试一些其他的测试框架,以及了解框架的一些原理,总之,要想正儿八经的写代码,单元测试时必不可少的(不只是写JS哦)。
你要请我喝一杯奶茶?
版权声明:自由转载-非商用-保持署名和原文链接。
本站文章均为本人原创,参考文章我都会在文中进行声明,也请您转载时附上署名。