什么是可测试代码

什么是可测试代码

我们理解的可测试代码指的是:

1、松耦合

2、短小的

3、可隔离的

我们会依照这三个原则来分析,怎么样编写可测试的代码?

一般来说,一个函数或者一个功能,如果越复杂,那么实现此功能需要的代码可能就会越多,代码量就会越多,出现潜在Bug的概率就越大。

因此,编写可测试代码的第一步就是让函数或者功能保持最小代码量,而保持最小代码量的方法就是让命令(Command)和查询(Query)分离。

我们首先需要理解的是什么是命令,什么是查询?

命令函数表示做什么(do something);而查询函数表示返回什么(return something);命令表示setter而查询表示getter。命令函数使用mock(spy)进行测试,而查询函数使用stub进行测试。

下面我们看一个例子:

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;
}

结论:一个函数有返回值就是查询,一个函数没有返回值就是命令。

有了命令和查询的基础知识,我们再来看看如何将第一个例子中的命令部分和查询部分隔离出来:

我们下一节在进行分析。欢迎讨论。

 

版权声明:著作权归作者所有。

thumb_up 0 | star_outline 0 | textsms 0