Stubbing factory being used in resolve

2019-03-04 18:32发布

问题:

I'm trying to unit test my states in an controller. What I want to do is stub out my items factory, since I have separate unit tests that cover that functionality. I'm having a hard time getting the $injector to actually inject the factory, but it seems like I'm letting the $provider know that I want to use my fake items object when it instantiates the controller. As a disclaimer I'm brand new to angular and would love some advice if my code looks bad.

Currently when I run the test I get the message:

Error: Unexpected request: GET /home.html
    No more request expected
        at $httpBackend (node_modules/angular-mocks/angular-mocks.js:1418:9)
        at n (node_modules/angular/angular.min.js:99:53)
        at node_modules/angular/angular.min.js:96:262
        at node_modules/angular/angular.min.js:131:20
        at m.$eval (node_modules/angular/angular.min.js:145:347)
        at m.$digest (node_modules/angular/angular.min.js:142:420)
        at Object.<anonymous> (spec/states/homeSpec.js:29:16)

It appears that my mocked items factory isn't being injected into the test. When I place a console.log line in the method I want to stub in the items factory I see that line being invoked.

The code I'm looking to test is as follows:

angular.module('todo', ['ui.router'])
// this is the factory i want to stub out...
.factory('items', ['$http', function($http){
  var itemsFactory = {};
  itemsFactory.getAll = function() {
    // ...specifically this method
  };
  return itemsFactory;
}])
.controller('TodoCtrl', ['$scope', 'items', function($scope, items) {
  // Do things
}])
.config(['$stateProvider', '$urlRouterProvider', function($stateProvider, $urlRouterProvider){
  $stateProvider
    .state('home', {
      url: '/home',
      templateUrl: '/home.html',
      controller: 'TodoCtrl',
      resolve: {
        items: ['items', function(items){
          // this is the invocation that i want to use my stubbed method
          return items.getAll();
        }]
      }
    });

  $urlRouterProvider.otherwise('home');
}]);

My test looks like this:

describe('home state', function() {

  var $rootScope, $state, $injector, state = 'home';
  var getAllStub = sinon.stub();
  var items = {
    getAll: getAllStub
  };

  beforeEach(function() {
    module('todo', function($provide) {
      $provide.value('items', items);
    });

    inject(function(_$rootScope_, _$state_, _$injector_) {
      $rootScope = _$rootScope_;
      $state = _$state_;
      $injector = _$injector_;
    });
  });

  it('should resolve items', function() {
    getAllStub.returns('getAll');

    $state.go(state);
    $rootScope.$digest();
    expect($state.current.name).toBe(state);

    expect($injector.invoke($state.current.resolve.items)).toBe('findAll');
  });
});

Thanks in advance for your help!

回答1:

Allowing real router in unit tests is a bad idea because it breaks the isolation and adds more moving parts. I personally consider $stateProvider, etc. stubs a better testing strategy.

The order matters in config blocks, service providers should be mocked before they will be injected in other modules. If the original modules have config blocks that override mocked service providers, the modules should be stubbed:

  beforeAll(function () {
    angular.module('ui.router', []);
  });

  beforeEach(function () {
    var $stateProviderMock = {
      state: sinon.stub().returnsThis()
    };

    module(function($provide) {
      $provide.constant('$stateProvider', $stateProviderMock);
    });
    module('todo');
  });

You just need to make sure that $stateProvider.state is called with expected configuration objects an arguments:

  it('should define home state', function () {
    expect($stateProviderMock.state.callCount).to.equal(1);

    let [homeStateName, homeStateObj] = $stateProviderMock.state.getCall(0).args;

    expect(homeStateName).to.equal('home');
    expect(homeState).to.be.an('object');

    expect(homeState.resolve).to.be.an('object');
    expect(homeState.resolve.items).to.be.an('array');

    let resolvedItems = $injector.invoke(homeState.resolve.items);
    expect(items.getAll).to.have.been.calledOnce;
    expect(resolvedItems).to.equal('getAll');

    ...
  });