10月阅读周·编写可测试的JavaScript代码:复杂度之代码大小篇
【摘要】 背景去年下半年,我在微信书架里加入了许多技术书籍,各种类别的都有,断断续续的读了一部分。没有计划的阅读,收效甚微。新年伊始,我准备尝试一下其他方式,比如阅读周。每月抽出1~2个非连续周,完整阅读一本书籍。这个“玩法”虽然常见且板正,但是有效,已经坚持阅读九个月。已读完书籍:《架构简洁之道》、《深入浅出的Node.js》、《你不知道的JavaScript(上卷)》、《你不知道的JavaScri...
背景
去年下半年,我在微信书架里加入了许多技术书籍,各种类别的都有,断断续续的读了一部分。
没有计划的阅读,收效甚微。
新年伊始,我准备尝试一下其他方式,比如阅读周。每月抽出1~2个非连续周,完整阅读一本书籍。
这个“玩法”虽然常见且板正,但是有效,已经坚持阅读九个月。
已读完书籍:《架构简洁之道》、《深入浅出的Node.js》、《你不知道的JavaScript(上卷)》、《你不知道的JavaScript(中卷)》、《你不知道的JavaScript(下卷)》、《数据结构与算法JavaScript描述》、《WebKit技术内幕》、《前端架构:从入门到微前端》、《秒懂算法:用常识解读数据结构与算法》、《JavaScript权威指南》、《JavaScript异步编程设计快速响应的网络应用》。
当前阅读周书籍:《编写可测试的JavaScript代码》。
代码大小
随着代码规模的增大,代码的复杂度也在增加,能够理解整个系统的人却在减少。随着模块数量的增加,集成测试变得越来越困难,模块交互的次数也在增加。这并不是很奇怪,因此,出现潜在Bug的首要因素是代码的大小。代码越多,出错的机会就越多。程序所需的代码总量可能不会改变,但每个方法里的语句数量却是可以改变的。每个文件里的代码数量同样也是可变的。太大的方法难以测试和维护,所以需要将其变小。理想情况下,函数没有副作用,并且其返回值(如果有的话)完全依赖于函数的参数。虽然代码几乎只存活于方法内,但编写的时候要时刻记住:“我该如何去测试这个代码?”
可以让函数保持最小代码量的一个方法是让命令(Command)和查询(Query)保持分离。命令函数表示做什么(do something),而查询函数则表示返回什么(return something)。也就是说,命令表示setter,查询表示getter。命令函数使用模(mock)进行测试,而查询函数使用桩(stub)进行测试(更多细节见第4章)。让这些概念保持分离,并提高可测试性,通过确保读写分离,可以实现良好的可伸缩性。如下是一个命令和查询分离的Node.js示例:
function configure(values) {
var fs = require('fs'),
config = { docroot: '/somewhere' };
key, 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 exi t exist or is not a directory!! **');
return;
}
// ... check other values ...
return config;
}
上述示例函数做了太多事情。默认配置值设置完之后,它继续检查这些值的有效性;事实上,下面的测试函数检查了另外5个值。每个测试方法太大,而且每次检查都是完全独立的。此外,对每个值的验证逻辑都包装在这一个函数中,是不可能进行隔离验证的。测试这种代码,需要对每个配置值的潜在值都要进行测试。同样,要让该函数发挥基本作用,需要很多单元测试,并在单元测试内部要包括所有的验证逻辑。很显然,随着时间的推移,配置值会增加得越来越多,只会导致代码变得越来越丑陋。另外,在try / catch内再抛出错误提示就会让try / catch变得一无是处。最后,返回值很怪异:没有值验证的话,会返回undefined;而通过验证时,则返回整个config对象。
将上述函数拆解成几个部分才是合理的解决方案。如下是一种方案:
function configure(values){
var config = { docroot: '/somewhere'},
key;
for (key in values){
config[key]=values[key];
}
return config;
}
function validateDocRoot(config) {
var fs = require('fs'),
stat;
stat = fs.statsync(config.docroot);
if(!stat.isdirectory()) {
throw new Error('is not valid');
}
}
function validateSomethingElse(config) {...}
上述代码,我们分离成两部分,一部分是设置函数(查询,并返回值),另外一部分是验证函数(命令,没有返回值;还有可能抛错)。将该函数分解成两个功能较小的函数,可以让我们的单元测试变得更具有针对性,且更加灵活。
现在,我们可以为每个要验证的函数编写单独的、可隔离的单元测试了,而不必把所有的验证都放在一个大的configure单元测试里了:
describe('validate value1', function () {
it('accepts the correct value', function () {
// some expects
});
it('rejects the incorrect value', function () {
// some expects
});
});
这是迈向可测试性的一大步,验证功能现在可以单独进行测试了,而不再需要包装大量普通的configure测试了。但是,从验证里分离出来的setter却可以完全避免通过验证。虽然有时候是可以的,但通常不能这样做。这时候,命令和查询分离就可以解决问题——在验证之前,我们不希望产生任何副作用。尽管单独的验证功能对测试来说是一件好事,但我们需要确保它们能被调用。
再次重构一下,可能类似于如下示例:
function configure(values) {
var config = { docRoot: '/somewhere' };
for (var key in values) {
config[key] = values[key];
}
validateDocRoot(config);
validateSomethingElse(config);
...
return config;
}
新的配置函数要么返回一个有效的配置对象,要么抛出一个错误。所有的验证函数都可以从configure函数中脱离出来单独进行测试。
最后一点可以改进的,是将config对象里的每个键都链接到validator函数,以便整个散列对象都在一个中心位置上:
var fields = {
docRoot: { validator: validateDocRoot, default: '/somewhere' },
somethingElse: { validator: validateSomethingElse },
};
function configure(values) {
for (var key in fields) {
if (typeof values[key] !== 'undefined') {
fields[key].validator(values[key]);
config[key] = values[key];
} else {
config[key] = fields[key].default;
}
}
return config;
}
这是一个极大的妥协。validator函数都可以很容易地进行测试,对象赋值的时候也都会调用,并且所有的数据都存储在一个中央位置。
总结
命令查询分离并非JavaScript界唯一的游戏,而且也并不总是可行的,但它通常是一个很好的起点。尽管可以通过JavaScript的巨大优势之一:事件,来实现命名查询的分离,但代码大小还是可以通过很多不同的方式进行管理的。
作者介绍
非职业「传道授业解惑」的开发者叶一一。
《趣学前端》、《CSS畅想》等系列作者。华夏美食、国漫、古风重度爱好者,刑侦、无限流小说初级玩家。
如果看完文章有所收获,欢迎点赞👍 | 收藏⭐️ | 留言📝。
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱:
cloudbbs@huaweicloud.com
- 点赞
- 收藏
- 关注作者
作者其他文章
评论(0)