如何单元测试AngularJS控制器外的注入器逻辑?含$http Mock方案
问题分析与解决方案
你当前测试失败的核心原因是:那段初始化代码是在测试运行前就全局自动执行的,而你在测试用例里设置的spyOn(angular, 'injector')是在module('app')加载后才生效,根本没覆盖到实际执行的初始化逻辑。要解决这个问题,我们需要先重构代码让它可测试,再针对性mock依赖来验证不同场景。
第一步:重构初始化代码,让它可测试
把原本全局执行的初始化逻辑封装成一个可调用的函数,这样我们可以在测试里手动触发它,而非页面加载时自动执行:
// 封装初始化逻辑为可调用函数 window.initApp = function() { var initInjector = angular.injector(['ng']); var $http = initInjector.get('$http'); // 注意:原代码里的response.user大概率是笔误,Angular $http返回的服务器数据在response.data里 $http.get('../api/user').then( function (response) { if (response.data.user.valid) { angular.bootstrap(document, ['app']); } }, function () { window.location.href = '../userError.html'; } ); }; // 生产环境下,页面加载完成后自动执行初始化 document.addEventListener('DOMContentLoaded', function() { window.initApp(); });
第二步:编写单元测试,mock依赖验证场景
现在我们可以在测试里手动调用initApp(),并mockangular.injector和$http的行为,分别测试成功、失败、用户无效三种场景:
describe('App Initialization', function() { beforeEach(function() { // 重置所有mock和spy // 1. mock angular.injector,返回我们控制的$http服务 spyOn(angular, 'injector').and.returnValue({ get: function(serviceName) { if (serviceName === '$http') { // 返回带mock then方法的$http return { get: jasmine.createSpy('$http.get').and.returnValue({ then: function(successCb, errorCb) { // 保存回调,方便测试时手动触发 this.successCallback = successCb; this.errorCallback = errorCb; return this; } }) }; } } }); // 2. spy angular.bootstrap,验证是否触发应用启动 spyOn(angular, 'bootstrap'); // 3. spy window.location.href,防止测试时真的跳转页面 spyOn(window.location, 'href').and.callFake(function() {}); }); it('should bootstrap the app when valid user is returned', function() { // 调用初始化函数 window.initApp(); // 获取$http.get的返回对象,触发成功回调 var httpGetSpy = angular.injector().get('$http').get; var thenable = httpGetSpy.calls.mostRecent().returnValue; thenable.successCallback({ data: { user: { valid: true } } }); // 验证应用启动逻辑被触发 expect(angular.bootstrap).toHaveBeenCalledWith(document, ['app']); // 验证跳转逻辑未触发 expect(window.location.href).not.toHaveBeenCalled(); }); it('should redirect to error page when $http request fails', function() { window.initApp(); var httpGetSpy = angular.injector().get('$http').get; var thenable = httpGetSpy.calls.mostRecent().returnValue; // 触发错误回调 thenable.errorCallback(); // 验证跳转逻辑被触发 expect(window.location.href).toHaveBeenCalledWith('../userError.html'); // 验证应用启动逻辑未触发 expect(angular.bootstrap).not.toHaveBeenCalled(); }); it('should NOT bootstrap the app when user is invalid', function() { window.initApp(); var httpGetSpy = angular.injector().get('$http').get; var thenable = httpGetSpy.calls.mostRecent().returnValue; thenable.successCallback({ data: { user: { valid: false } } }); expect(angular.bootstrap).not.toHaveBeenCalled(); expect(window.location.href).not.toHaveBeenCalled(); }); });
关键细节说明
- 封装初始化函数:解决了测试时代码提前执行导致spy失效的问题,让我们能完全控制初始化逻辑的执行时机。
- mock $http服务:因为这里的$http是从全局
angular.injector获取的,而非Angular应用内的依赖,所以需要手动mock它的get和then方法,方便我们手动触发回调。 - 避免真实跳转:通过
spyOn(window.location.href)并设置空的callFake,防止测试过程中真的跳转到错误页面。 - 修正response路径:原代码里的
response.user是常见笔误,Angular的$http响应中,服务器返回的数据始终在response.data属性下,生产环境也需要修正这一点才能正常工作。
内容的提问来源于stack exchange,提问作者mccarthyj
相关产品推荐
相关产品推荐

