当前位置: 首页 > 科技观察

编写可测试的JavaScript代码

时间:2023-03-22 15:42:53 科技观察

无论我们使用与Node一起工作的测试框架,例如Mocha或Jasmine,还是在像PhantomJS这样的无头浏览器中运行依赖于DOM的测试,与之前相比,我们有更好的方法对JavaScript进行单元测试.然而,这并不意味着我们要测试的代码和我们的工具一样简单!组织和编写易于测试的代码需要一些努力和计划,但受函数式编程的启发,我们发现了一些模式,当我们需要测试我们的代码时,这些模式可以帮助我们避免那些“陷阱”。在本文中,我们将了解一些有用的技巧和模式,以帮助我们用JavaScript编写可测试的代码。保持业务逻辑和显示逻辑分离对于基于JavaScript的浏览器应用程序,主要工作之一是监听最终用户触发的DOM事件,然后运行一些业务逻辑并将结果显示在页面上。给予反馈。在设置DOM事件侦听器的地方,有时很想编写一个完成所有这些工作的匿名函数。这样做的问题是,为了测试匿名函数,您必须模拟DOM事件。这不仅会增加代码行数,还会增加运行测试的时间。相反,编写一个命名函数并将其传递给事件处理程序。这样,您可以直接针对这个命名函数编写测试用例,而不会触发伪造的DOM事件。这不仅适用于DOM。浏览器和Node中的许多API旨在触发和侦听事件,或等待其他类型的异步工作完成。根据经验,如果您编写大量匿名回调,您的代码可能不容易测试。//hardtotest$('button').on('click',()=>{$.getJSON('/path/to/data').then(data=>{$('#my-list').html('results:'+data.join(','));});});//可测试;我们可以直接运行fetchThingstoseifit//生成AJAX请求而无需触发DOM//事件,我们可以直接运行showThings以查看它//在DOM中显示数据而不执行AJAX请求)$('按钮单击',()=>fetchThings(showThings));functionfetchThings(回调){$.getJSON('/path/to/data')。then(callback);}functionshowThings(data){$('#my-list').html('results:'+data.join(','));}异步代码使用回调或promise示例代码上面,我们重构的fetchThings函数运行一个AJAX请求以异步的方式完成大部分工作。这意味着我们无法运行该函数并测试它的行为是否符合我们的预期,因为我们不知道它何时完成。解决这个问题最常见的方法是将回调函数作为参数传递给该函数作为异步调用。这样,在您的单元测试中,您可以在传递的回调函数中运行一些断言。组织异步代码的另一种常见且日益流行的方法是使用PromiseAPI。幸运的是,$.ajax和大多数jQuery异步函数已经返回Promises,因此它已经涵盖了最常见的用例。//难以测试;我们不知道AJAX请求将运行多长时间functionfetchData(){$.ajax({url:'/path/to/data'});}//testable;我们传入一个回调函数,然后在其中运行断言返回的Promise已解决,我们可以运行断言函数fetchDataWithPromise(){return$.ajax({url:'/path/to/data'});}来避免副作用来编写接受参数并返回值的函数只依赖于那些参数,比如传递数字传递一个数学公式并得到结果。如果你的函数依赖于一些外部状态(比如类实例的属性或一些文件的内容),那么你必须在测试这个函数之前设置一些状态,并且在测试用例中需要更多的设置。您必须认为正在运行的代码不会修改相同的状态。此外,您需要避免编写修改外部状态的函数,例如写入文件或将数据保存到数据库。这避免了影响您测试其他代码的能力的副作用。一般来说,最好的方法是把控制副作用和代码放在一起,让“表面积”尽可能小。对于类和对象实例,类方法的副作用应该限制在被测试的类实例的范围内。//难以测试;我们必须先设置一个globalListOfCars对象和一个名为#list-of-models的DOM结构,然后才能测试此代码models').html(models.join(','));}//方便测试;我们传递一个参数并测试它的返回值而不设置任何全局变量或检查任何DOM结果函数buildModelsString(cars){constmodels=cars.map(car=>car.model);returnmodels.join(',');}使用函数中的依赖注入,有一个通用的模式可以用来减少外部状态的使用,这就是依赖注入——函数的所有外部需求都通过函数参数传递给函数。//依赖于外部状态数据连接实例;难测functionupdateRow(rowId,data){myGlobalDatabaseConnector.update(rowId,data);}//将数据库连接实例作为参数传递给函数;易于测试。functionupdateRow(rowId,data,databaseConnector){databaseConnector.update(rowId,data);}使用依赖注入的主要好处之一是你可以在单元测试中传入模拟对象,这不会导致真正的副作用(在这个在此示例中,更新数据库行),您只需要断言您的模拟对象的行为符合预期。每个函数都有一个独特的用途,将长函数分解为一系列更小的、单一职责的函数。这样我们就可以更方便地测试每个函数是否正确,而不再希望一个大函数把所有事情都做对了再返回结果。在函数式编程中,将几个单一职责函数拼凑在一起的行为称为组合。Underscore.js甚至有一个名为_.compose的函数,它将函数串成一个函数列表,将每个函数的结果作为输入传递给下一个函数。//难以测试函数createGreeting(name,location,age){letgreeting;if(location==='Mexico'){greeting='!Hola';}else{greeting='Hello';}greeting+=''+name.toUpperCase()+'!';greeting+='Youare'+age+'yearsold.';returngreeting;}//方便测试函数getBeginning(location){if(location==='Mexico'){return'?Hola';}else{return'Hello';}}functiongetMiddle(name){return''+name.toUpperCase()+'!';}functiongetEnd(age){return'Youare'+age+'yearsold.';}functioncreateGreeting(name,location,age){returngetBeginning(location)+getMiddle(name)+getEnd(age);}不要改变参数在JavaScript中,数组和对象是通过引用传递的,而不是通过值传递的,所以他们是可变的。这意味着当您将对象或数组作为参数传递给函数时,您的代码和使用您传递的对象或数组的函数都能够修改内存中的相同数组或对象。这意味着当您测试自己的代码时,您必须相信您调用的任何函数都不会修改您的对象。每次您添加一些修改同一对象的新代码时,跟踪对象应该是什么样子变得越来越困难,因此测试它们也变得越来越困难。相反,当您有一个需要使用对象或数组的函数时,您应该将代码中的对象或数组视为只读的。您可以根据需要创建新的对象或数组,然后对齐填充。或者,使用Undersocre或Lodash复制传入的对象或数组,然后对齐。更好的是,使用像Immutable.js这样的工具来创建只读数据结构。//修改传入对象functionupperCaseLocation(customerInfo){customerInfo.location=customerInfo.location.toUpperCase();returncustomerInfo;}//返回一个新对象functionupperCaseLocation(customerInfo){return{name:customerInfo.name,location:customerInfo.location.toUpperCase(),age:customerInfo.age};}在编码之前编写测试在编码之前编写单元测试的过程称为测试驱动开发(TDD)。大量开发人员发现TDD非常有用。通过首先编写测试用例,您强迫自己从使用您的代码的开发人员的角度考虑您公开的API,这也有助于您确保您只编写足够的代码来满足测试用例而不是解决方案“过度建设”,从而引入不必要的复杂性。实际上,TDD作为一门学科可能很难涵盖所有代码更改。但是当它看起来值得尝试时,这是确保所有代码都可测试的好方法。总结在编写和测试复杂的JavaScript应用程序时,我们都知道有一些容易遇到的“陷阱”,但我希望通过这些提示和提醒,让我们的代码尽可能保持简单和功能,并且我们可以这样做才能让测试覆盖率高,让整体的代码复杂度很低!