11月阅读周·编写可测试的JavaScript代码:单元测试之真实场景测试篇
背景
去年下半年,我在微信书架里加入了许多技术书籍,各种类别的都有,断断续续的读了一部分。
没有计划的阅读,收效甚微。
新年伊始,我准备尝试一下其他方式,比如阅读周。每月抽出1~2个非连续周,完整阅读一本书籍。
这个“玩法”虽然常见且板正,但是有效,已经坚持阅读十个月。
已读完书籍:《架构简洁之道》、《深入浅出的Node.js》、《你不知道的JavaScript(上卷)》、《你不知道的JavaScript(中卷)》、《你不知道的JavaScript(下卷)》、《数据结构与算法JavaScript描述》、《WebKit技术内幕》、《前端架构:从入门到微前端》、《秒懂算法:用常识解读数据结构与算法》、《JavaScript权威指南》、《JavaScript异步编程设计快速响应的网络应用》。
当前阅读周书籍:《编写可测试的JavaScript代码》。
真实场景测试
遗憾的是,这只是一个理想的函数,而并不都像老生常谈的sum函数一样。但注意,即便是只有一行代码,并且无任何依赖的函数的单元测试,也不会像我们想象得那么简单。这再次证明了单元测试的难度,尤其是JavaScript单元测试:一行代码的函数就有这么多陷阱,那真实场景的函数又如何呢?
依赖项
几乎所有函数都有单元测试不想测试的外部依赖。单元测试者可以利用模(mock)和桩(stub)提取依赖关系。请注意,我说的是“单元测试可以提取”,因为被测试代码的作者也有一些工具可以从代码中提取相关的依赖,一旦这些工作都完成了,剩余的事情就是对隔离代码编写单元测试从而进行测试了。
使用命令查询分离强调模与桩之间的差异:mock用于命令,而sub则用于查询。
测试替身
测试替身是一个通用术语,描述的是使用stub或mock模拟依赖对象进行测试(类似于电影替身)。在同一时间,替身(double)可以用stub表示,也可以用mock表示,以确保外部方法和API被调用、并判断被调用了多少次、捕获所调用的参数、并返回响应。在方法如何被调用方面,能记录或捕获相关信息的测试替身,被称为间谍(spy)。
mock对象
mock对象用于验证函数能够正确调用外部API。单元测试通过引入mock对象验证被测试函数传递正确的参数(通过类型或值)给外部对象时的结果。在命令查询中,对测试命令进行模拟(mock);而在事件集线器中,所模拟的测试事件就会被触发。
查看如下示例:
// actual production code:
function buyNowClicked(event) {
hasSubscribers.fire('addToShoppingCart', { item: event.target.id });
}
Y.one('#products').delegate('click', buyNowClicked, '.buy-now-button');
/* and in your test code: */
testAddToShoppingCart: function () {
var hub = Y.Mock();
Y.Mock.expect(hub, { method: 'fire', args: ['addToShoppingCart', Y.Mock.Value.String] });
}
Y.one('.buy-now-button').simulate('click');
Y.Mock.verify(hub);
上述代码测试了Buy Now按钮(带有buy-nowbutton样式的元素)的流程。按钮被点击时,预计会触发一个带有字符串参数的addToShoppingCart事件。在这里,我们模拟了事件集线器对象,并且fire方法按预期进行调用,并传入了一个字符串参数(Buy Now按钮的ID)。
stub对象
stub对象用于向被测试函数返回所封装的值。stub对象不关注外部对象方法是如何调用的;stub对象只是返回所选择的封装对象。该返回对象通常是调用者对测试用例进行正向测试所预期的对象,或进行负向测试所需要的对象。
使用stub进行测试时,需要使用stub对象替代真实的对象。在其生命周期中唯一的乐趣就是返回一个封装值,以保持所测试的方法能够继续下去。
查看如下代码:
function addToShoppingCart(item) {
if (item.inStock()) {
this.cart.push(item);
return item;
} else {
return null;
}
}
测试上述代码的一个方法是模拟(stub)item.inStock方法,并测试完所有的代码路径:
testAddOk: function () {
var shoppingCart = new ShoppingCart();
var item = {
isStock: function () {
return true;
},
};
var back = shoppingCart.addToShoppingCart(item);
Y.Assert.areSame(back, item, 'Item not returned');
Y.ArrayAssert.contains(back, item, 'Item not pushed into cart!');
},
testAddNok: function () {
var shoppingCart = new ShoppingCart();
var item = {
isStock: function () {
return false;
},
};
var back = shoppingCart.addToShoppingCart(item);
Y.Assert.isNull(back, 'Item returned is not null');
Y.ArrayAssert.isEmpty(shoppingCart.cart, 'Item pushed into cart!');
};
这种测试通过stub出的item对象捕获了函数的两个分支。当然,不仅仅只有参数可以stub,一般来说,所有的外部依赖都可以stub或mock。
spy对象
spy一般是封装“真正的”的对象,并覆盖一些方法以便让调用者通过。spy通常是附加到真正的对象上,并拦截一些方法的调用(有时甚至只有拦截方法调用的特定参数),并返回封装响应或跟踪该方法被调用的次数。没被拦截的方法则按正常流程对真正的对象进行处理。如果需要模拟(mock)一个含有“重”操作的对象,并对实际对象进行很简单的轻量级操作的话,spy是一个很好的选择。
在测试过程中,并不仅限于这些测试替身!如果需要创建一个既有mock特性又有stub特性的测试替身的话,那就这样做吧!如果想跟踪外部函数被调用多少次,那就要spy!通常测试替身用于特定的一个测试或一组测试,对有限的mock或stub来说其不可重用。但在很多情况下,在适当的测试中使用它们是必要的。
Jasmine测试框架对创建测试替身和spy有良好的支持。本章稍后我们会练习这些特性。
异步测试
使用YUI Test
testAsync: function () {
var test = (this.myButton = Y.onc('#button'));
myButton.on('click', function () {
this.resume(function () {
Y.Assert.isTrue(true, 'You sunk my battleship!');
});
});
myButton.simulate('click');
this.walt(2000);
}
上述代码将模拟一个按钮点击,并等待两秒。过后如果测试没有恢复,就是失败了;否则,测试就会恢复(希望成功)并执行一个断言测试。显然,与模拟在按钮上点击相比,我们可以做更多有趣的事情,并验证处理程序是否被调用。
如果代码在事件响应中有很多CSS(Cascading Style Sheets,层叠样式表)样式的操作,那就需要进行异步测试。然而,对于任何核心UI测试来说,使用像Selenium这样的工具进行集成测试更可取,因为在某些情况下单元测试会更脆弱或不完整。此外,并不是所有的DOM事件都可以模拟;在这些情况下,进行其他类型的测试是必需的。
无论如何,在测试基于事件的代码(类似如下代码)时,异步测试是有必要的:
hun.on('log', function (severity, message) {
console.log(severity + ':' + message);
});
测试上述代码,需要模拟(mock)全局的console对象(这是命令,而不是查询):
console = Y.Mock();
testLogHandler: function () {
var sev = 'DEBUG',
message = 'TEST';
Y.Mock.expect(console, { method: 'log', arguments: [sev, message] });
hun.fire('log', sev, message);
this.wait(function () {
Y.Mock.verify(console);
}, 1000);
}
可以使用“真实的”事件集线器,因为在这里并没有测试集线器功能。这个测试只是验证模拟对象的log方法能够接收预期参数并调用,这就是模拟对象所做的:测试接口是否被正确使用。
总结
JavaScript编程在很大程度上依赖于事件。异步测试涉及到暂停测试流程以及等待事件触发,然后在事件处理程序中再恢复流程,可能还要抛出一到两个断言。
作者介绍
非职业「传道授业解惑」的开发者叶一一。
《趣学前端》、《CSS畅想》等系列作者。华夏美食、国漫、古风重度爱好者,刑侦、无限流小说初级玩家。
如果看完文章有所收获,欢迎点赞👍 | 收藏⭐️ | 留言📝。
- 点赞
- 收藏
- 关注作者
评论(0)