【问题标题】:How should the run block be dealt with in Angular unit tests?在 Angular 单元测试中应该如何处理 run 块?
【发布时间】:2015-11-17 02:32:12
【问题描述】:

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

我认为,如果您正在测试一个组件,您不会希望同时测试 run 块,因为 unit 测试应该只测试一个 单位。这是真的吗?

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

运行块:

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 块在加载模块时运行,因此在设置间谍之前调用 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();
});

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

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

尝试 3:

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

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

这是他的示例代码:

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 块使用模拟;我在别处需要我真正的Auth 服务,以便我可以对其进行测试。

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


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


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 块问题,因为该模块没有run 块代码,但它允许我测试我的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'));

【问题讨论】:

  • 与您的问题无关,但您应该知道.run 不会等待$http 完成。如果应用程序中的任何内容都依赖于结果,那么您就有了竞争条件。通常,如果您使用的是ngRouteui.router,您将使用resolve

标签: javascript angularjs unit-testing karma-runner


【解决方案1】:

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

正确。

我认为如果你正在测试一个组件,你不会想要 同时测试运行块,因为单元测试是 应该只测试一个单元。这是真的吗?

同样正确,因为现在您正在有效地测试 Auth 和您的运行块的集成,并且两者之间没有隔离。

如果是这样,有没有办法阻止运行块运行?我的 研究让我认为答案是否定的 模块加载时块总是运行,但也许有办法 覆盖它。如果没有,我将如何测试运行块?

在实施时,您不能阻止运行块运行。但是,通过一些小的重构仍然有可能,因为您的问题最终是模块化之一。如果无法看到您的模块声明,我会想象它看起来像这样:

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 工厂。

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

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');

【讨论】:

  • 谢谢!几个问题(我也会自己调查,但也想问你):1)Re:嘲笑Auth.getCurrentUser,有必要使用$provide吗?我不能在加载mean-auth 并注入Auth 之后只使用spyOn(Auth, 'getCurrentUser') 吗? 2) 目前,我的Auth 工厂正在使用$rootScope 来跟踪当前登录的用户。通过使用mock-auth 模块,它的$rootScope 不会与mean-starter$rootScope 相同,对吧?我认为这是 $rootScope 的一个很好的用例,但我现在正在考虑如何重构它。
  • 我编辑了我的问题以包括我当前的auth.factory.js。在考虑了更多之后,我不确定单独的 mean-auth 模块将如何工作。对于前 3 种方法,我可以在 .post/.get 之后返回承诺,然后在控制器中执行其余逻辑。但是对于getCurrentUser,我想根据当前用户是否已经可用,有条件地发出 HTTP 请求。但是mean-auth 怎么知道当前用户呢?我想我可以在其他地方进行这项检查?
  • 1.你不需要$provide 来测试mean-auth,因为它的状态不再受你的运行块的影响。但是,您确实需要使用它来测试mean-starter,因为运行块会立即触发。 2. $rootScope 将是相同的。 Angular 的模拟 module 在创建注入器之前收集已定义的模块,因此 $rootScope 作为“应用程序”的父(即顶级)范围存在。
  • mean-auth 会知道当前用户,因为在被mean-starter 加载时,它的$rootScope 是相同的(用外行的话来说,根范围“附加”到@987654358 的任何位置@ 已声明,因此其中的所有模块都将继承它)。
  • 谢谢,我了解 (2) 以及您所说的 $rootScope 已附加到 ng-app。不过,我仍然不清楚(1)。为什么我的编辑中的代码不起作用? a) 我的想法是我会监视Auth.requestCurrentUser(必须更改方法名称),因此当mean-starterrun 块运行时,它将是一个存根,而不是真正的函数。 b) 我收到一条错误消息 Error: Injector already created, can not register a module!。那是因为我使用了两次inject() 吗?如果是这样,我将如何解决这个问题?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2010-11-30
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-06-08
  • 2021-06-19
  • 2018-09-14
相关资源
最近更新 更多