AngularJS测试中的模拟依赖项
核心要点
- AngularJS 天生就考虑到了测试,其内置的依赖注入机制使得每个组件都可使用任何 JavaScript 测试框架(如 Jasmine)进行测试。
- 单元测试中的模拟涉及隔离测试代码片段的功能,这可能具有挑战性,因为依赖项来自不同的来源。AngularJS 中的模拟通过
angular-mocks
模块简化了,该模块为一组常用的 AngularJS 服务提供了模拟。 - AngularJS 中的服务模拟可以通过获取实际服务的实例并侦听服务的方法,或者使用
$provide
实现模拟服务来完成。后者方法更可取,可以避免调用服务的实际方法实现。 - AngularJS 中的提供程序模拟遵循与服务模拟类似的规则。测试中必须实现
$get
方法。如果测试文件中不需要$get
函数中定义的功能,则可以将其赋值为空函数。 - 全局对象(例如全局“window”对象的一部分或由第三方库创建的对象)可以通过注入它们到
$window
或使用全局对象创建值或常量并根据需要注入它们来实现模拟。
AngularJS 的设计理念中就包含了测试。框架的源代码经过了非常充分的测试,并且使用该框架编写的任何代码也都是可测试的。内置的依赖注入机制使得用 AngularJS 编写的每个组件都可进行测试。AngularJS 应用程序中的代码可以使用任何现有的 JavaScript 测试框架进行单元测试。最常用于测试 AngularJS 代码的框架是 Jasmine。本文中的所有示例代码片段都是使用 Jasmine 编写的。如果您在 Angular 项目中使用任何其他测试框架,您仍然可以应用本文中讨论的思想。
本文假设您已经具备单元测试和测试 AngularJS 代码的经验。您不必是测试专家。如果您对测试有基本的了解,并且可以为 AngularJS 应用程序编写一些简单的测试用例,那么您可以继续阅读本文。
模拟在单元测试中的作用
每个单元测试的任务都是隔离地测试一段代码的功能。隔离被测系统有时可能具有挑战性,因为依赖项可能来自不同的来源,我们需要充分理解要模拟的对象的职责。
在 JavaScript 等非静态类型语言中,模拟很困难,因为不容易理解要模拟的对象的结构。同时,它也提供了灵活性,即仅模拟被测系统当前正在使用的对象的某一部分,而忽略其余部分。
AngularJS 测试中的模拟
由于 AngularJS 的主要目标之一是可测试性,核心团队为此付出了额外的努力,使测试更容易,并在 angular-mocks
模块中为我们提供了一组模拟。此模块包含围绕一组 AngularJS 服务(例如 $http
、$timeout
、$animate
等)的模拟,这些服务广泛用于任何 AngularJS 应用程序中。此模块减少了开发人员编写测试所需的大量时间。
在为真实的业务应用程序编写测试时,这些模拟非常有帮助。同时,它们不足以测试整个应用程序。我们需要模拟框架中但未被模拟的任何依赖项——来自第三方插件的依赖项、全局对象或在应用程序中创建的依赖项。本文将介绍一些关于模拟 AngularJS 依赖项的技巧。
模拟服务
服务是 AngularJS 应用程序中最常见的依赖项类型。您可能已经知道,服务在 AngularJS 中是一个重载的术语。它可能指服务、工厂、值、常量或提供程序。我们将在下一节讨论提供程序。服务可以通过以下方式之一进行模拟:
- 使用注入块获取实际服务的实例并侦听服务的方法。
- 使用
$provide
实现模拟服务。
我不喜欢第一种方法,因为它可能导致调用服务的实际方法实现。我们将使用第二种方法来模拟以下服务:
angular.module('sampleServices', []) .service('util', function() { this.isNumber = function(num) { return !isNaN(num); }; this.isDate = function(date) { return (date instanceof Date); }; });
以下代码片段创建了上述服务的模拟:
module(function($provide) { $provide.service('util', function() { this.isNumber = jasmine.createSpy('isNumber').andCallFake(function(num) { // 模拟实现 }); this.isDate = jasmine.createSpy('isDate').andCallFake(function(num) { // 模拟实现 }); }); }); // 获取模拟服务的引用 var mockUtilSvc; inject(function(util) { mockUtilSvc = util; });
尽管上面的示例使用 Jasmine 创建间谍,但您可以使用 Sinon.js 替换它,实现等效的功能。
最好在加载测试所需的所有模块后创建所有模拟。否则,如果在一个已加载的模块中定义了一个服务,则实际实现会覆盖模拟实现。
常量、工厂和值可以使用 $provide.constant
、$provide.factory
和 $provide.value
分别进行模拟。
模拟提供程序
模拟提供程序类似于模拟服务。编写提供程序时必须遵循的所有规则也必须在模拟它们时遵循。考虑以下提供程序:
angular.module('mockingProviders',[]) .provider('sample', function() { var registeredVals = []; this.register = function(val) { registeredVals.push(val); }; this.$get = function() { function getRegisteredVals() { return registeredVals; } return { getRegisteredVals: getRegisteredVals }; }; });
以下代码片段为上述提供程序创建了一个模拟:
module(function($provide) { $provide.provider('sample', function() { this.register = jasmine.createSpy('register'); this.$get = function() { var getRegisteredVals = jasmine.createSpy('getRegisteredVals'); return { getRegisteredVals: getRegisteredVals }; }; }); }); // 获取提供程序的引用 var sampleProviderObj; module(function(sampleProvider) { sampleProviderObj = sampleProvider; });
获取提供程序和其他单例的引用的区别在于,提供程序在此时不会在 inject()
块中可用,因为提供程序此时已转换为工厂。我们可以使用 module()
块获取它们的对象。
在定义提供程序的情况下,测试中也必须实现 $get
方法。如果您在测试文件中不需要 $get
函数中定义的功能,则可以将其赋值为空函数。
模拟模块
如果要在测试文件中加载的模块需要一堆其他模块,则除非加载所有必需的模块,否则无法加载被测模块。加载所有这些模块有时会导致测试失败,因为某些实际的服务方法可能会从测试中调用。为了避免这些困难,我们可以创建虚拟模块来加载被测模块。
例如,假设以下代码表示一个添加了示例服务的模块:
angular.module('sampleServices', []) .service('util', function() { this.isNumber = function(num) { return !isNaN(num); }; this.isDate = function(date) { return (date instanceof Date); }; });
以下代码是示例服务的测试文件中的 beforeEach
块:
module(function($provide) { $provide.service('util', function() { this.isNumber = jasmine.createSpy('isNumber').andCallFake(function(num) { // 模拟实现 }); this.isDate = jasmine.createSpy('isDate').andCallFake(function(num) { // 模拟实现 }); }); }); // 获取模拟服务的引用 var mockUtilSvc; inject(function(util) { mockUtilSvc = util; });
或者,我们也可以将服务的模拟实现添加到上面定义的虚拟模块中。
模拟返回 Promise 的方法
如果不使用 Promise,编写端到端的 Angular 应用程序可能很困难。测试依赖于返回 Promise 的方法的代码片段成为一项挑战。普通的 Jasmine 间谍会导致某些测试用例失败,因为被测函数会期望一个具有实际 Promise 结构的对象。
可以使用另一个返回具有静态值的 Promise 的异步方法来模拟异步方法。考虑以下工厂:
angular.module('mockingProviders',[]) .provider('sample', function() { var registeredVals = []; this.register = function(val) { registeredVals.push(val); }; this.$get = function() { function getRegisteredVals() { return registeredVals; } return { getRegisteredVals: getRegisteredVals }; }; });
我们将测试上述工厂中的 getData()
函数。正如我们所看到的,它依赖于服务 dataSourceSvc
的方法 getAllItems()
。我们需要在测试 getData()
方法的功能之前模拟服务和方法。
$q
服务具有 when()
和 reject()
方法,允许使用静态值来解析或拒绝 Promise。这些方法在模拟返回 Promise 的方法的测试中非常有用。以下代码片段模拟了 dataSourceSvc
工厂:
module(function($provide) { $provide.provider('sample', function() { this.register = jasmine.createSpy('register'); this.$get = function() { var getRegisteredVals = jasmine.createSpy('getRegisteredVals'); return { getRegisteredVals: getRegisteredVals }; }; }); }); // 获取提供程序的引用 var sampleProviderObj; module(function(sampleProvider) { sampleProviderObj = sampleProvider; });
$q
Promise 在下一个 digest 周期后完成其操作。digest 周期在实际应用程序中不断运行,但在测试中则不会。因此,我们需要手动调用 $rootScope.$digest()
以强制执行 Promise。以下代码片段显示了一个示例测试:
angular.module('first', ['second', 'third']) // util 和 storage 分别在 second 和 third 中定义 .service('sampleSvc', function(utilSvc, storageSvc) { // 服务实现 });
模拟全局对象
全局对象来自以下来源:
- 全局“window”对象的一部分的对象(例如,localStorage、indexedDb、Math 等)。
- 由第三方库(如 jQuery、underscore、moment、breeze 或任何其他库)创建的对象。
默认情况下,全局对象无法模拟。我们需要遵循某些步骤才能使它们可模拟。
我们可能不想模拟 Math 对象或 _
(由 Underscore 库创建)的实用程序对象,因为它们的操作不执行任何业务逻辑、不操作 UI,也不与数据源通信。但是,必须模拟诸如 $.ajax、localStorage、WebSockets、breeze 和 toastr 之类对象。因为如果没有模拟这些对象,这些对象会在执行单元测试时执行其实际操作,这可能会导致一些不必要的 UI 更新、网络调用,有时还会导致测试代码中的错误。
由于依赖注入,Angular 中编写的每一部分代码都是可测试的。DI 允许我们传递任何遵循实际对象 shim 的对象,只是为了使被测代码在执行时不会中断。如果可以注入全局对象,则可以模拟它们。有两种方法可以使全局对象可注入:
- 将
$window
注入到需要全局对象的 service/controller 中,并通过$window
访问全局对象。例如,以下服务通过$window
使用 localStorage:
angular.module('sampleServices', []) .service('util', function() { this.isNumber = function(num) { return !isNaN(num); }; this.isDate = function(date) { return (date instanceof Date); }; });
- 使用全局对象创建一个值或常量,并在需要的地方注入它。例如,以下代码是 toastr 的常量:
module(function($provide) { $provide.service('util', function() { this.isNumber = jasmine.createSpy('isNumber').andCallFake(function(num) { // 模拟实现 }); this.isDate = jasmine.createSpy('isDate').andCallFake(function(num) { // 模拟实现 }); }); }); // 获取模拟服务的引用 var mockUtilSvc; inject(function(util) { mockUtilSvc = util; });
我更喜欢使用常量而不是值来包装全局对象,因为常量可以注入到配置块或提供程序中,并且常量不能被装饰。
以下代码片段显示了 localStorage 和 toastr 的模拟:
angular.module('mockingProviders',[]) .provider('sample', function() { var registeredVals = []; this.register = function(val) { registeredVals.push(val); }; this.$get = function() { function getRegisteredVals() { return registeredVals; } return { getRegisteredVals: getRegisteredVals }; }; });
结论
模拟是在任何语言中编写单元测试的重要组成部分之一。正如我们所看到的,依赖注入在测试和模拟中起着重要作用。代码必须以一种方式组织,以便轻松测试其功能。本文列出了在测试 AngularJS 应用程序时模拟最常见的一组对象。与本文相关的代码可从 GitHub 下载。
关于在 AngularJS 测试中模拟依赖项的常见问题解答 (FAQ)
在 AngularJS 测试中模拟依赖项的目的是什么?
在 AngularJS 测试中模拟依赖项是单元测试的关键部分。它允许开发人员隔离被测代码并模拟其依赖项的行为。这样,您可以测试代码如何与其依赖项交互,而无需实际调用它们。当依赖项复杂、缓慢或具有您希望在测试期间避免的副作用时,这尤其有用。通过模拟这些依赖项,您可以专注于在受控环境中测试代码的功能。
如何在 AngularJS 中创建一个模拟服务?
在 AngularJS 中创建模拟服务涉及在模块配置中使用 $provide
服务。您可以使用 $provide
服务的 value
、factory
或 service
方法来定义服务的模拟实现。这是一个基本示例:
module(function($provide) { $provide.provider('sample', function() { this.register = jasmine.createSpy('register'); this.$get = function() { var getRegisteredVals = jasmine.createSpy('getRegisteredVals'); return { getRegisteredVals: getRegisteredVals }; }; }); }); // 获取提供程序的引用 var sampleProviderObj; module(function(sampleProvider) { sampleProviderObj = sampleProvider; });
在这个例子中,我们使用 $provide.value
方法来定义 myService
的模拟实现。在测试期间,将使用此模拟服务代替实际服务。
(其余的FAQ问题,由于篇幅限制,请逐个提出,我会尽力提供简洁明了的答案。)
以上是AngularJS测试中的模拟依赖项的详细内容。更多信息请关注PHP中文网其他相关文章!

热AI工具

Undresser.AI Undress
人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover
用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool
免费脱衣服图片

Clothoff.io
AI脱衣机

Video Face Swap
使用我们完全免费的人工智能换脸工具轻松在任何视频中换脸!

热门文章

热工具

记事本++7.3.1
好用且免费的代码编辑器

SublimeText3汉化版
中文版,非常好用

禅工作室 13.0.1
功能强大的PHP集成开发环境

Dreamweaver CS6
视觉化网页开发工具

SublimeText3 Mac版
神级代码编辑软件(SublimeText3)

JavaScript是现代Web开发的基石,它的主要功能包括事件驱动编程、动态内容生成和异步编程。1)事件驱动编程允许网页根据用户操作动态变化。2)动态内容生成使得页面内容可以根据条件调整。3)异步编程确保用户界面不被阻塞。JavaScript广泛应用于网页交互、单页面应用和服务器端开发,极大地提升了用户体验和跨平台开发的灵活性。

Python和JavaScript开发者的薪资没有绝对的高低,具体取决于技能和行业需求。1.Python在数据科学和机器学习领域可能薪资更高。2.JavaScript在前端和全栈开发中需求大,薪资也可观。3.影响因素包括经验、地理位置、公司规模和特定技能。

实现视差滚动和元素动画效果的探讨本文将探讨如何实现类似资生堂官网(https://www.shiseido.co.jp/sb/wonderland/)中�...

学习JavaScript不难,但有挑战。1)理解基础概念如变量、数据类型、函数等。2)掌握异步编程,通过事件循环实现。3)使用DOM操作和Promise处理异步请求。4)避免常见错误,使用调试技巧。5)优化性能,遵循最佳实践。

JavaScript的最新趋势包括TypeScript的崛起、现代框架和库的流行以及WebAssembly的应用。未来前景涵盖更强大的类型系统、服务器端JavaScript的发展、人工智能和机器学习的扩展以及物联网和边缘计算的潜力。

如何在JavaScript中将具有相同ID的数组元素合并到一个对象中?在处理数据时,我们常常会遇到需要将具有相同ID�...

zustand异步操作中的数据更新问题在使用zustand状态管理库时,经常会遇到异步操作导致数据更新不及时的问题。�...
