11月阅读周·编写可测试的JavaScript代码:单元测试之真实场景测试篇

举报
叶一一 发表于 2024/11/21 18:33:22 2024/11/21
【摘要】 背景去年下半年,我在微信书架里加入了许多技术书籍,各种类别的都有,断断续续的读了一部分。没有计划的阅读,收效甚微。新年伊始,我准备尝试一下其他方式,比如阅读周。每月抽出1~2个非连续周,完整阅读一本书籍。这个“玩法”虽然常见且板正,但是有效,已经坚持阅读十个月。已读完书籍:《架构简洁之道》、《深入浅出的Node.js》、《你不知道的JavaScript(上卷)》、《你不知道的JavaScri...

背景

去年下半年,我在微信书架里加入了许多技术书籍,各种类别的都有,断断续续的读了一部分。

没有计划的阅读,收效甚微。

新年伊始,我准备尝试一下其他方式,比如阅读周。每月抽出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畅想》等系列作者。华夏美食、国漫、古风重度爱好者,刑侦、无限流小说初级玩家。
如果看完文章有所收获,欢迎点赞👍 | 收藏️ | 留言📝

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。