什么是可测试代码

我们理解的可测试代码指的是:
1、松耦合
2、短小的
3、可隔离的
我们会依照这三个原则来分析,怎么样编写可测试的代码?
一般来说,一个函数或者一个功能,如果越复杂,那么实现此功能需要的代码可能就会越多,代码量就会越多,出现潜在Bug的概率就越大。
因此,编写可测试代码的第一步就是让函数或者功能保持最小代码量,而保持最小代码量的方法就是让命令(Command)和查询(Query)分离。
我们首先需要理解的是什么是命令,什么是查询?
命令函数表示做什么(do something);而查询函数表示返回什么(return something);命令表示setter而查询表示getter。命令函数使用mock进行测试,而查询函数使用stub进行测试。
下面我们看一个例子:
function configure(values){
var fs = require('fs');
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;
}
这个函数的主要功能是将参数values
的值复制到config
对象,然后检查config.docRoot
是否是一个目录,然后检查其他参数,最后返回config
对象。
另外还有一点就是检查的代码,如果config.docRoot
不是一个目录,会抛出一个错误,并且返回一个undefined
这个逻辑虽然没啥不对,但是有点奇怪。
我们首先思考一下这个问题?这个函数的功能会不会太多?这个功能里面哪些属于命令而哪些又属于查询?
我们看看这个函数的测试代码:
describe('configure test', ()=>{
it('undef if docRoot does not exist', function(){
expect(configure({ docRoot: '/xxx'})).toBeUndefined();
});
it('not undef if docRoot does exist', function(){
expect(configure({ docRoot: '/tmp'})).not.toBeUndefined();
});
it('adds values to config hash', function(){
var config = configure({ docRoot: '/xxx' , zany:'crazy'});
expect(config).not.toBeUndefined();
expect(config.zany).toEqual('crazy');
expect(config.docRoot).toEqual('/xxx');
});
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;
}
结论:一个函数有返回值就是查询,一个函数没有返回值就是命令。
有了命令和查询的基础知识,我们再来看看如何将第一个例子中的命令部分和查询部分隔离出来:
我们下一节在进行分析。欢迎讨论。