javascript - 在 Angular 单元测试中应该如何处理运行 block ?

标签 javascript angularjs unit-testing karma-runner

我的理解是,当您在 Angular 单元测试中加载模块时,run block 会被调用。

我想如果你正在测试一个组件,你不会希望同时测试 run block ,因为 unit 测试应该只是测试一个单元。是真的吗?

如果是这样,有没有办法阻止 run block 运行?我的研究使我认为答案是“否”,并且 run block 总是在加载模块时运行,但也许有一种方法可以覆盖它。如果没有,我将如何测试 run block ?

运行 block :

function run(Auth, $cookies, $rootScope) {
  $rootScope.user = {};
  Auth.getCurrentUser();
}

Auth.getCurrentUser:

getCurrentUser: function() {
  // user is logged in
  if (Object.keys($rootScope.user).length > 0) {
    return $q.when($rootScope.user);
  }
  // user is logged in, but page has been refreshed and $rootScope.user is lost
  if ($cookies.get('userId')) {
    return $http.get('/current-user')
      .then(function(response) {
        angular.copy(response.data, $rootScope.user);
        return $rootScope.user;
      })
    ;
  }
  // user isn't logged in
  else  {
    return $q.when({});
  }
}

auth.factory.spec.js

describe('Auth Factory', function() {
  var Auth, $httpBackend, $rootScope, $cookies, $q;
  var user = {
    username: 'a',
    password: 'password',
  };
  var response = {
    _id: 1,
    local: {
      username: 'a',
      role: 'user'
    }
  };

  function isPromise(el) {
    return !!el.$$state;
  }

  beforeEach(module('mean-starter', 'ngCookies', 'templates'));
  beforeEach(inject(function(_Auth_, _$httpBackend_, _$rootScope_, _$cookies_, _$q_) {
    Auth = _Auth_;
    $httpBackend = _$httpBackend_;
    $rootScope = _$rootScope_;
    $cookies = _$cookies_;
    $q = _$q_;
  }));
  afterEach(function() {
    $httpBackend.verifyNoOutstandingExpectation();
    $httpBackend.verifyNoOutstandingRequest();
  });

  it('#signup', function() {
    $rootScope.user = {};
    $httpBackend.expectPOST('/users', user).respond(response);
    spyOn(angular, 'copy').and.callThrough();
    spyOn($cookies, 'put').and.callThrough();
    var retVal = Auth.signup(user);
    $httpBackend.flush();
    expect(angular.copy).toHaveBeenCalledWith(response, $rootScope.user);
    expect($cookies.put).toHaveBeenCalledWith('userId', 1);
    expect(isPromise(retVal)).toBe(true);
  });

  it('#login', function() {
    $rootScope.user = {};
    $httpBackend.expectPOST('/login', user).respond(response);
    spyOn(angular, 'copy').and.callThrough();
    spyOn($cookies, 'put').and.callThrough();
    var retVal = Auth.login(user);
    $httpBackend.flush();
    expect(angular.copy).toHaveBeenCalledWith(response, $rootScope.user);
    expect($cookies.put).toHaveBeenCalledWith('userId', 1);
    expect(isPromise(retVal)).toBe(true);
  });

  it('#logout', function() {
    $httpBackend.expectGET('/logout').respond();
    spyOn(angular, 'copy').and.callThrough();
    spyOn($cookies, 'remove');
    Auth.logout();
    $httpBackend.flush();
    expect(angular.copy).toHaveBeenCalledWith({}, $rootScope.user);
    expect($cookies.remove).toHaveBeenCalledWith('userId');
  });

  describe('#getCurrentUser', function() {
    it('User is logged in', function() {
      $rootScope.user = response;
      spyOn($q, 'when').and.callThrough();
      var retVal = Auth.getCurrentUser();
      expect($q.when).toHaveBeenCalledWith($rootScope.user);
      expect(isPromise(retVal)).toBe(true);
    });
    it('User is logged in but page has been refreshed', function() {
      $cookies.put('userId', 1);
      $httpBackend.expectGET('/current-user').respond(response);
      spyOn(angular, 'copy').and.callThrough();
      var retVal = Auth.getCurrentUser();
      $httpBackend.flush();
      expect(angular.copy).toHaveBeenCalledWith(response, $rootScope.user);
      expect(isPromise(retVal)).toBe(true);
    });
    it("User isn't logged in", function() {
      $rootScope.user = {};
      $cookies.remove('userId');
      spyOn($q, 'when').and.callThrough();
      var retVal = Auth.getCurrentUser();
      expect($q.when).toHaveBeenCalledWith({});
      expect(isPromise(retVal)).toBe(true);
    });
  });
});

尝试 1:

beforeEach(module('mean-starter', 'ngCookies', 'templates'));
beforeEach(inject(function(_Auth_, _$httpBackend_, _$rootScope_, _$cookies_, _$q_) {
  Auth = _Auth_;
  $httpBackend = _$httpBackend_;
  $rootScope = _$rootScope_;
  $cookies = _$cookies_;
  $q = _$q_;
}));
beforeEach(function() {
  spyOn(Auth, 'getCurrentUser');
});
afterEach(function() {
  expect(Auth.getCurrentUser).toHaveBeenCalled();
  $httpBackend.verifyNoOutstandingExpectation();
  $httpBackend.verifyNoOutstandingRequest();
});

这行不通。 run block 在加载模块时运行,因此在设置 spy 之前调用 Auth.getCurrentUser()

Expected spy getCurrentUser to have been called.

尝试 2:

beforeEach(inject(function(_Auth_, _$httpBackend_, _$rootScope_, _$cookies_, _$q_) {
  Auth = _Auth_;
  $httpBackend = _$httpBackend_;
  $rootScope = _$rootScope_;
  $cookies = _$cookies_;
  $q = _$q_;
}));
beforeEach(function() {
  spyOn(Auth, 'getCurrentUser');
});
beforeEach(module('mean-starter', 'ngCookies', 'templates'));
afterEach(function() {
  expect(Auth.getCurrentUser).toHaveBeenCalled();
  $httpBackend.verifyNoOutstandingExpectation();
  $httpBackend.verifyNoOutstandingRequest();
});

这不起作用,因为在加载我的应用模块之前无法注入(inject) Auth

Error: [$injector:unpr] Unknown provider: AuthProvider <- Auth

尝试 3:

如您所见,这里存在先有鸡还是先有蛋的问题。我需要在加载模块之前注入(inject) Auth 并设置 spy ,但我不能,因为在加载模块之前无法注入(inject) Auth。

This博客文章提到先有鸡还是先有蛋的问题,并提供了一个有趣的潜在解决方案。作者建议我应该在加载模块之前使用 $provide 手动创建我的 Auth 服务。因为我正在创建服务,而不是注入(inject)它,所以我可以在加载模块之前完成它,并且我可以设置 spy 。然后在加载模块时,它会使用这个创建的模拟服务。

这是他的示例代码:

describe('example', function () {
    var loggingService;
    beforeEach(function () {
        module('example', function ($provide) {
            $provide.value('loggingService', {
                start: jasmine.createSpy()
            });
        });
        inject(function (_loggingService_) {
            loggingService = _loggingService_;
        });
    });
    it('should start logging service', function() {
        expect(loggingService.start).toHaveBeenCalled();
    });
});

问题是我需要我的 Auth 服务!我只想将模拟的用于 run block ;我需要在别处使用真正的 Auth 服务,以便对其进行测试。

我想我可以使用 $provide 创建实际的 Auth 服务,但感觉不对。


最后一个问题 - 无论我最终使用什么代码来处理这个 run block 问题,我有没有办法将它提取出来,这样我就不必重新编写它我的每个规范文件?我能想到的唯一方法是使用某种全局函数。


auth.factory.js

angular
  .module('mean-starter')
  .factory('Auth', Auth)
;

function Auth($http, $state, $window, $cookies, $q, $rootScope) {
  return {
    signup: function(user) {
      return $http
        .post('/users', user)
        .then(function(response) {
          angular.copy(response.data, $rootScope.user);
          $cookies.put('userId', response.data._id);
          $state.go('home');
        })
      ;
    },
    login: function(user) {
      return $http
        .post('/login', user)
        .then(function(response) {
          angular.copy(response.data, $rootScope.user);
          $cookies.put('userId', response.data._id);
          $state.go('home');
        })
      ;
    },
    logout: function() {
      $http
        .get('/logout')
        .then(function() {
          angular.copy({}, $rootScope.user);
          $cookies.remove('userId');
          $state.go('home');
        })
        .catch(function() {
          console.log('Problem logging out.');
        })
      ;
    },
    getCurrentUser: function() {
      // user is logged in
      if (Object.keys($rootScope.user).length > 0) {
        return $q.when($rootScope.user);
      }
      // user is logged in, but page has been refreshed and $rootScope.user is lost
      if ($cookies.get('userId')) {
        return $http.get('/current-user')
          .then(function(response) {
            angular.copy(response.data, $rootScope.user);
            return $rootScope.user;
          })
        ;
      }
      // user isn't logged in
      else  {
        return $q.when({});
      }
    }
  };
}

编辑 - 失败尝试 + 成功尝试:

beforeEach(module('auth'));
beforeEach(inject(function(_Auth_) {
  Auth = _Auth_;
  spyOn(Auth, 'requestCurrentUser');
}));
beforeEach(module('mean-starter', 'ngCookies', 'templates'));
beforeEach(inject(function(_Auth_, _$httpBackend_, _$rootScope_, _$cookies_, _$q_) {
  // Auth = _Auth_;
  $httpBackend = _$httpBackend_;
  $rootScope = _$rootScope_;
  $cookies = _$cookies_;
  $q = _$q_;
}));
// beforeEach(function() {
//   spyOn(Auth, 'getCurrentUser');
// });
afterEach(function() {
  expect(Auth.getCurrentUser).toHaveBeenCalled();
  $httpBackend.verifyNoOutstandingExpectation();
  $httpBackend.verifyNoOutstandingRequest();
});

我不确定为什么这行不通(独立于使用 inject 两次的问题)。

我试图绕开必须使用 $provide 的问题,因为最初我觉得它很老套/很奇怪。不过再想一想,我现在觉得 $provide 很好,按照你的建议使用 mock-auth 太棒了!!!两者都为我工作。

auth.factory.spec.js 中,我刚刚加载了 auth 模块(我称它为 auth,而不是 mean-auth) 而无需加载 mean-starter。这没有 run block 问题,因为该模块没有 run block 代码,但它允许我测试我的 Auth工厂。在其他地方,这有效:

beforeEach(module('mean-starter', 'templates', function($provide) {
  $provide.value('Auth', {
    requestCurrentUser: jasmine.createSpy()
  });
}));

奇妙的 mock-auth 解决方案也是如此:

auth.factory.mock.js

angular
  .module('mock-auth', [])
  .factory('Auth', Auth)
;

function Auth() {
  return {
    requestCurrentUser: jasmine.createSpy()
  };
}

user.service.spec.js

beforeEach(module('mean-starter', 'mock-auth', 'templates'));

最佳答案

My understanding is that when you load your module in Angular unit tests, the run block gets called.

正确。

I'd think that if you're testing a component, you wouldn't want to simultaneously be testing the run block, because unit tests are supposed to just test one unit. Is that true?

也是正确的,因为现在您正在有效地测试 Auth 和您的运行 block 的集成,并且彼此之间没有隔离。

If so, is there a way to prevent the run block from running? My research leads me to think that the answer is "no", and that the run block always runs when the module is loaded, but perhaps there's a way to override this. If not, how would I test the run block?

作为实现,不,你不能阻止运行 block 运行。但是,由于您的问题最终是模块化之一,因此进行一些小的重构仍然是可能的。在看不到您的模块声明的情况下,我想它看起来像这样:

angular.module('mean-starter', ['ngCookies'])

  .factory('Auth', function($cookies) {
    ...
  });

  .run(function(Auth, $rootScope) {
    ...
  });

此模式可以分解为模块以支持可测试性(和模块可重用性):

angular.module('mean-auth', ['ngCookies'])

  .factory('Auth', function() {
    ...
  });

angular.module('mean-starter', ['mean-auth'])

  .run(function(Auth, $rootScope) {
    ...
  });

这现在允许您通过仅将 mean-auth 模块加载到其测试中来单独测试您的 Auth 工厂。

虽然这解决了运行 block 干扰 Auth 单元测试的问题,但您仍然面临模拟 Auth.getCurrentUser 以测试运行的问题隔离阻断。您引用的博客文章是正确的,因为您应该寻求利用模块的配置阶段来 stub /监视运行阶段使用的依赖项。因此,在您的测试中:

module('mean-starter', function ($provide) {
  $provide.value('Auth', {
    getCurrentUser: jasmine.createSpy()
  });
});

关于您的最后一个问题,您可以通过将它们声明为模块来创建可重用的模拟。例如,如果您想为 Auth 创建一个可重用的模拟工厂,您可以在单元测试之前加载的单独文件中定义它:

angular.module('mock-auth', [])

 .factory('Auth', function() {
   return {
     getCurrentUser: jasmine.createSpy()
   };
 });

然后将它加载到您需要它的任何模块之后的测试中,因为 Angular 将覆盖具有相同名称的任何服务:

module('mean-starter', 'mock-auth');

关于javascript - 在 Angular 单元测试中应该如何处理运行 block ?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/32169240/

相关文章:

javascript - 如何调试通过 AJAX 调用添加到 DOM 中的 JS 代码?

javascript - 在 Javascript 中将日期格式的字符串转换为 Date 对象的简单方法?

angularjs - 从非 Angular javascript函数访问 Angular 范围变量

C# + 模拟服务层?

javascript - JavaScript 中的 [keys[i]] 是什么?

javascript - 匹配最多 2 位小数的数字的正则表达式

javascript - 使用 ng-if 隐藏导航栏

javascript - 查询ISO格式的日期问题

Python:制作全局单元测试功能

javascript - useFakeTimers 和异步回调