编写可测试的JavaScript代码

简介:

无论我们使用和Node配合在一起的测试框架,例如Mocha或者Jasmine,还是在像PhantomJS这样的无头浏览器中运行依赖于DOM的测试,和以前相比,我们有更好的方式来对JavaScript进行单元测试。

然而,这并不意味着我们要测试的代码就像我们的工具那样容易!组织和编写易于测试的代码需要花费一些精力和并对其进行规划,但是在函数式编程的启发下,我们发现了一些模式,当我们需要测试我们的代码时,这些模式可以帮助我们避免那些“坑”。在这篇文章中,我们会查看一些有用的小贴士和模式,来帮助我们在JavaScript中编写可测试的代码。

保持业务逻辑和显示逻辑分离

对于基于JavaScript的浏览器应用程序来说,其中一项主要工作就是监听终端用户触发的DOM事件,然后通过运行一些业务逻辑并在页面上显示结果,以此对用户做出反馈。在建立DOM事件监听器的地方,有时会诱惑你编写一个匿名函数来完成所有这些工作。这样带来的问题是为了测试匿名函数,你不得不去模拟DOM事件。这样不仅会增加代码行数,而且会增加测试运行的时间。

与之相反,编写一个有名字的函数,然后将其传给事件处理器。通过这种方式,你可以直接针对这个有名字的函数编写测试用例,而不用去触发一个假的DOM事件。

这不仅仅可以应用到DOM上。在浏览器和Node上的很多API,都被设计成触发和监听事件,或者等待其它类型的异步工作完成。凭经验说,如果你编写了大量的匿名回调函数,那么你的代码可能不会容易被测试。

 
 
  1. // hard to test 
  2. $('button').on('click', () => { 
  3.     $.getJSON('/path/to/data'
  4.         .then(data => { 
  5.             $('#my-list').html('results: ' + data.join(', ')); 
  6.         }); 
  7. }); 
  8.   
  9. // testable; we can directly run fetchThings to see if it 
  10. // makes an AJAX request without having to trigger DOM 
  11. // events, and we can run showThings directly to see that it 
  12. // displays data in the DOM without doing an AJAX request 
  13. $('button').on('click', () => fetchThings(showThings)); 
  14.   
  15. function fetchThings(callback) { 
  16.     $.getJSON('/path/to/data').then(callback); 
  17.   
  18. function showThings(data) { 
  19.     $('#my-list').html('results: ' + data.join(', ')); 
  20.  

对异步代码使用回调或者Promise

在上述的示例代码中,我们经过重构的函数fetchThings会运行一个AJAX请求,以异步的方式完成了大部分工作。这意味着我们不能运行函数并测试它是否按照我们预期的那样运行,因为我们不知道它什么时候运行完。

解决这个问题最常见的方法,是向函数中传递一个回调函数作为参数,作为异步调用。这样,在你的单元测试中,你可以在传递的回调函数中运行一些断言。

另外一种常见并且越来越流行的组织异步代码方法,是使用Promise API的方式。幸运的是,$.ajax和其它大部分jQuery的异步函数已经返回了Promise对象,因此它已经涵盖了大部分常见的用例。

 
 
  1. // 很难测试;我们不知道AJAX请求会运行多长时间 
  2.   
  3. function fetchData() { 
  4.     $.ajax({ url: '/path/to/data' }); 
  5.   
  6. //可测试的;我们传入一个回调函数,然后在其中运行断言 
  7.   
  8. function fetchDataWithCallback(callback) { 
  9.     $.ajax({ 
  10.         url: '/path/to/data'
  11.         success: callback, 
  12.     }); 
  13.   
  14. //同样可测试的:在返回的Promise解析完后,我们可以运行断言 
  15.   
  16. function fetchDataWithPromise() { 
  17.     return $.ajax({ url: '/path/to/data' }); 
  18.  

避免副作用

要编写那些使用参数并且返回值仅仅依赖那些参数的函数,就像将数字传入数学公式,然后取得结果。如果你的函数依赖于一些外部的状态(例如类实例的属性或者某些文件的内容),那么你在测试这个函数之前,就不得不去设置一些状态,在测试用例中需要更多的设置。你不得不去认为那些正在运行的代码不会修改同一个的状态。

同样,你需要避免编写那些会修改外部状态的函数,例如向文件写入内容或者向数据库保存数据。这会避免一些副作用,来影响你测试其他代码的能力。一般来说,最好是将副作用和代码控制在一起,让“表面积”尽可能小。对于类和对象实例来说,类方法的副作用应该被限制在被测试的类实例的范围内。

 
 
  1. // 很难测试;我们不得不设置一个globalListOfCars对象和一个名为#list-of-models的DOM结构,然后才能测试这段代码 
  2.   
  3. function processCarData() { 
  4.     const models = globalListOfCars.map(car => car.model); 
  5.     $('#list-of-models').html(models.join(', ')); 
  6.   
  7. // 容易测试;我们传递一个参数然后测试它的返回值,而不需要设置任何全局变量或者检查任何DOM结果 
  8.   
  9. function buildModelsString(cars) { 
  10.     const models = cars.map(car => car.model); 
  11.     return models.join(','); 
  12.  

使用依赖注入

在函数中,有一种通用的模式,可以用来降低对外部状态的使用,这就是依赖注入 —— 将函数的所有外部需要都通过函数参数的方式传递给函数。

 
 
  1. // 依赖于一个外部状态数据连接实例;很难测试 
  2.   
  3. function updateRow(rowId, data) { 
  4.     myGlobalDatabaseConnector.update(rowId, data); 
  5.   
  6. // 将数据库连接实例作为参数传递给函数;很容易测试。 
  7.   
  8. function updateRow(rowId, data, databaseConnector) { 
  9.     databaseConnector.update(rowId, data); 
  10.  

使用依赖注入的一个主要好处,是你可以在单元测试中传入mock对象,这样就不会导致真的副作用(在这个例子中,就是更新数据库行),你只需要断言你的mock对象是按照期望的方式运行即可。

为每一个函数设置一个唯一的目的

将长函数分解成一系列小的、单一职责的函数。这样我们可以更容易的去测试每一个函数是否是正确的,而不再希望一个大函数在返回结果之前就正确的做了所有的事情。

在函数式编程中,将几个单一职责的函数拼在一起的行为称作“组合”。Underscore.js甚至有一个名为_.compose的函数,它将一个函数列表中的函数串在一起,将每一函数的返回结果作为输入传递给下一个函数。

 
 
  1. // 很难测试 
  2.   
  3. function createGreeting(name, location, age) { 
  4.     let greeting; 
  5.     if (location === 'Mexico') { 
  6.         greeting = '!Hola'
  7.     } else { 
  8.         greeting = 'Hello'
  9.     } 
  10.   
  11.     greeting += ' ' + name.toUpperCase() + '! '
  12.   
  13.     greeting += 'You are ' + age + ' years old.'
  14.   
  15.     return greeting; 
  16.   
  17. // 很容易测试 
  18.   
  19. function getBeginning(location) { 
  20.     if (location === 'Mexico') { 
  21.         return '¡Hola'
  22.     } else { 
  23.         return 'Hello'
  24.     } 
  25.   
  26. function getMiddle(name) { 
  27.     return ' ' + name.toUpperCase() + '! '
  28.   
  29. function getEnd(age) { 
  30.     return 'You are ' + age + ' years old.'
  31.   
  32. function createGreeting(name, location, age) { 
  33.     return getBeginning(location) + getMiddle(name) + getEnd(age); 
  34.  

不要改变参数

在JavaScript中,数组和对象传递的是引用,而非值,因此它们是可变的。这意味着当你将对象或者数组作为参数传递给函数时,你的代码和使用你传递的对象或数组的函数,都有能力去修改内存中同一个数组或者对象。这意味着当你测试你自己的代码时,你必须信任所有你调用的函数中,没有任何函数会修改你的对象。每当你添加一些新的可以修改同一个对象的代码时,跟踪对象应该是什么样子就会变得越来越困难,从而更难去测试它们。

相反,当你有一个函数需要使用对象或者数组时,你应该在代码中对待对象或者数组就像它们是只读的。你可以根据需要创建新的对象或者数组,然后对齐填充。或者,使用Undersocre或者Lodash去对传入的对象或者数组做一个拷贝,然后再对齐进行操作。更好的选择是,使用一些像Immutable.js这样的工具,去创建只读的数据结构。

 
 
  1. // 修改了传入的对象 
  2.   
  3. function upperCaseLocation(customerInfo) { 
  4.     customerInfo.location = customerInfo.location.toUpperCase(); 
  5.     return customerInfo; 
  6.   
  7. // 返回了一个新的对象 
  8.   
  9. function upperCaseLocation(customerInfo) { 
  10.     return { 
  11.         name: customerInfo.name
  12.         location: customerInfo.location.toUpperCase(), 
  13.         age: customerInfo.age 
  14.     }; 
  15.  

在编码之前先写测试

在编码之前先写单元测试的过程被称作测试驱动开发(TDD)。大量的开发者发现TDD非常有用。

通过先编写测试用例,你就强迫自己从使用你代码的开发者角度来考虑你要暴露的API,它还帮助你确保你只会编写足够的代码来满足测试用例的要求,而不要对解决方案“过度施工”,从而带来不必要的复杂性。

在实践中,TDD作为一条纪律,要覆盖所有的代码改动可能会比较困难。但是当它看上去值得尝试的时候,这就是一个很好的方式来保证你的所有代码都是可测试的。

总结

在编写和测试复杂的JavaScript应用的时候,我们都知道有一些很容易遇到的“陷阱”,但我希望通过这些贴士和提醒,可以让我们的代码尽量简单和函数化,我们可以做到让测试覆盖率很高,让整体的代码复杂性很低!




作者:伯乐专栏/Wing
来源:51CTO
目录
相关文章
|
1月前
|
JSON JavaScript 前端开发
JavaScript原生代码处理JSON的一些高频次方法合集
JavaScript原生代码处理JSON的一些高频次方法合集
|
2月前
|
Java 关系型数据库 数据库连接
Mybatis+MySQL动态分页查询数据经典案例(含代码以及测试)
Mybatis+MySQL动态分页查询数据经典案例(含代码以及测试)
37 1
|
6天前
|
Web App开发 JavaScript 前端开发
《手把手教你》系列技巧篇(三十九)-java+ selenium自动化测试-JavaScript的调用执行-上篇(详解教程)
【5月更文挑战第3天】本文介绍了如何在Web自动化测试中使用JavaScript执行器(JavascriptExecutor)来完成Selenium API无法处理的任务。首先,需要将WebDriver转换为JavascriptExecutor对象,然后通过executeScript方法执行JavaScript代码。示例用法包括设置JS代码字符串并调用executeScript。文章提供了两个实战场景:一是当时间插件限制输入时,用JS去除元素的readonly属性;二是处理需滚动才能显示的元素,利用JS滚动页面。还给出了一个滚动到底部的代码示例,并提供了详细步骤和解释。
30 10
|
17天前
|
jenkins Devops 测试技术
单元测试与质量保证:确保Visual Basic代码的健壮性
【4月更文挑战第27天】在VB开发中,单元测试是保证代码质量和软件健壮性的关键。本文介绍了单元测试的基础,包括其定义和好处,如提高代码质量、促进重构。接着,讨论了MSTest、NUnit和xUnit等VB单元测试工具。遵循TDD原则和最佳实践,编写独立、有针对性的测试,并注重测试速度和覆盖率。通过示例展示了如何在Visual Studio中设置和运行测试。最后,提到了持续集成和自动化测试工具,如Jenkins和静态代码分析工具,以提升软件开发效率和质量。单元测试不仅是技术手段,更是提升团队协作和软件工程水平的文化体现。
|
3天前
|
程序员 测试技术
程序员难以一次性写好代码并持续修复Bug,主要源于软件的高复杂性、需求不确定性、测试局限性和技术能力限制。
【5月更文挑战第11天】程序员难以一次性写好代码并持续修复Bug,主要源于软件的高复杂性、需求不确定性、测试局限性和技术能力限制。复杂的系统易产生意外问题,需求变化导致初始设计难完备,测试无法覆盖所有情况,而技术更新和个体能力差异也会引入错误。因此,持续调试和优化是保证软件质量的关键步骤。
9 0
|
3天前
|
算法 数据可视化 API
LabVIEWCompactRIO 开发指南33 测试和调试LabVIEW FPGA代码
LabVIEWCompactRIO 开发指南33 测试和调试LabVIEW FPGA代码
|
4天前
|
JavaScript 前端开发 测试技术
编写JavaScript模块化代码主要涉及将代码分割成不同的文件或模块,每个模块负责处理特定的功能或任务
【5月更文挑战第10天】编写JavaScript模块化代码最佳实践:使用ES6模块或CommonJS(Node.js),组织逻辑相关模块,避免全局变量,封装细节。利用命名空间和目录结构,借助Webpack处理浏览器环境的模块。编写文档和注释,编写单元测试以确保代码质量。通过这些方法提升代码的可读性和可维护性。
9 3
|
5天前
|
JavaScript 前端开发 Java
《手把手教你》系列技巧篇(四十)-java+ selenium自动化测试-JavaScript的调用执行-下篇(详解教程)
【5月更文挑战第4天】本文介绍了如何使用JavaScriptExecutor在自动化测试中实现元素高亮显示。通过创建并执行JS代码,可以改变元素的样式,例如设置背景色和边框,以突出显示被操作的元素。文中提供了一个Java示例,展示了如何在Selenium中使用此方法,并附有代码截图和运行效果展示。该技术有助于跟踪和理解测试过程中的元素交互。
8 0
|
10天前
|
测试技术
使用CLion创建Cmake项目,使用GoogleTest和GoogleMock对代码进行测试
使用CLion创建Cmake项目,使用GoogleTest和GoogleMock对代码进行测试
22 3
|
14天前
|
JavaScript 前端开发 开发工具
【JavaScript 与 TypeScript 技术专栏】TypeScript 如何提升 JavaScript 代码的可读性与可维护性
【4月更文挑战第30天】TypeScript 提升 JavaScript 代码的可读性和可维护性,主要通过静态类型系统、增强代码组织、智能提示与错误检测、文档化和在大型项目中的优势。静态类型减少误解,类和接口提供结构,智能提示提高编码效率,类型注解充当内置文档。在大型项目中,TypeScript 降低理解差异,平滑迁移现有 JavaScript 项目,助力提高开发效率和项目质量。