什么是可测试代码

我们理解的可测试代码指的是:
1、松耦合
2、短小的
3、可隔离的
我们会依照这三个原则来分析,怎么样编写可测试的代码?
一般来说,一个函数或者一个功能,如果越复杂,那么实现此功能需要的代码可能就会越多,代码量就会越多,出现潜在Bug的概率就越大。
因此,编写可测试代码的第一步就是让函数或者功能保持最小代码量,而保持最小代码量的方法就是让命令(Command)和查询(Query)分离。
我们首先需要理解的是什么是命令,什么是查询?
命令函数表示做什么(do something);而查询函数表示返回什么(return something);命令表示setter而查询表示getter。
下面我们看一个例子:
require("@fatso83/mini-mocha").install();
const sinon = require("sinon");
const { assert } = require("@sinonjs/referee");
var fs = require('fs');
//用例函数
function configure(values){
var config = { docRoot : '/somewhere'};
var key;
var stat;
for (key in values) {
config[key] = values[key];
}
try {
stat = fs.statSync(config.docRoot);
if (!stat.isDirectory()) {
throw new Error('Is not valid');
}
} catch(e) {
console.log('** '+ config.docRoot+ ' does not exist or is not a directory.');
return ;
}
//check other values ......
return config;
}
//用siono模拟出来的一个stubs,这样就可以在测试的时候不依赖于nodejs的fs模块
var statSync= sinon.stub(fs , 'statSync').callsFake(()=>{
return {
isDirectory: function(){
return true;
}
};
});
//以上的模拟只是测试了正常的情况,实际上应该有一个模拟来验证异常的情况,
//比如以上的返回对象的isDirectory函数返回false
const config = configure({docRoot:'/global'});
sinon.assert.calledOnce(statSync);
assert.equals(config,{docRoot:'/global'})
console.log('config:',config);
这个函数的主要功能是将参数values
的值复制到config
对象,然后检查config.docRoot
是否是一个目录,然后检查其他参数,最后返回config
对象。
另外还有一点就是检查的代码,如果config.docRoot
不是一个目录,会抛出一个错误,并且返回一个undefined
这个逻辑虽然没啥不对,但是有点奇怪。
我们首先思考一下这个问题?这个函数的功能会不会太多?这个功能里面哪些属于命令而哪些又属于查询?
我们看看这个函数的测试代码:
require("@fatso83/mini-mocha").install();
const { expect } = require('jest');
const { assert } = require("@sinonjs/referee");
var fs = require('fs');
//用例函数
function configure(values){
var config = { docRoot : '/somewhere'};
var key;
var stat;
for (key in values) {
config[key] = values[key];
}
try {
stat = fs.statSync(config.docRoot);
if (!stat.isDirectory()) {
throw new Error('Is not valid');
}
} catch(e) {
console.log('** '+ config.docRoot+ ' does not exist or is not a directory.');
return ;
}
//check other values ......
return config;
}
describe('configure test', ()=>{
it('undef if docRoot does not exist', function(){
assert.isUndefined(configure({ docRoot: '/xxx'}));
});
it('not undef if docRoot does exist', function(){
assert.isObject(configure({ docRoot: '/tmp'}));
});
it('adds values to config hash', function(){
var config = configure({ docRoot: '/tmp' , zany:'crazy'});
assert.isObject(config);
assert.equals(config.zany,'crazy');
assert.equals(config.docRoot,'/tmp')
});
it('varifies value1 good ...', function(){
});
it('varifies value1 bad ...', function(){
});
/** 其他验证测试 */
});
注意到没有:这个测试中,需要测试的功能太多,既有设置值,也有验证值。导致如此复杂的一个原因就是没有将命令和查询分离;同时,这里有一个经验性的技巧。
如果你发现您的代码很难写单元测试,那么这意味着您需要重构您的代码,重构的目标就是让单元测试更容易编写。(这是一个反复迭代的过程)
我们首先理解什么是命令,以及什么是查询?
/**
* 没有返回值,属于命令
*
*/
function warn(thing) {
console.log(['WARN:',thing].join(' '));
}
/**
* 没有返回值,属于命令
*
*/
function fail(thing) {
throw new Error(thing);
}
/**
* 有返回值,属于查询
*
*/
function add(a,b) {
return a + b;
}
结论:
有了命令和查询的基础知识,我们再来看看如何将第一个例子中的命令部分和查询部分隔离出来:
我们下一节在进行分析。欢迎讨论。