3

I'm having troubling testing a controller's value that's set within a promise returned by a service. I'm using Sinon to stub the service (Karma to run the tests, Mocha as the framework, Chai for assertions).

I'm less interested in a quick fix than I am in understanding the problem. I've read around quite a bit, and I have some of my notes below the code and the test.

Here's the code.

.controller('NavCtrl', function (NavService) {
  var vm = this;
  NavService.getNav()
    .then(function(response){
      vm.nav = response.data;
    });
})

.service('NavService', ['$http', function  ($http) {
  this.getNav = function () {
    return $http.get('_routes');
  };
}]);

Here's the test:

describe('NavCtrl', function () {

  var scope;
  var controller;
  var NavService;
  var $q;

  beforeEach(module('nav'));

  beforeEach(inject(function($rootScope, $controller, _$q_, _NavService_){
    NavService = _NavService_;
    scope = $rootScope.$new();
    controller = $controller;
  }));

  it('should have some data', function () {

    var stub = sinon.stub(NavService, 'getNav').returns($q.when({
      response: {
        data: 'data'
      }
    }));

    var vm = controller("NavCtrl", {
      $scope: scope,
      NavService: NavService
    });

    scope.$apply();

    stub.callCount.should.equal(1);
    vm.should.be.defined;
    vm.nav.should.be.defined;

  });

});

The stub is being called, i.e. that test passes, and vm is defined, but vm.nav never gets data and the test fails. How I'm handling the stubbed promise is, I think, the culprit. Some notes:

  • Based on reading elsewhere, I'm calling scope.$apply to set the value, but since scope isn't injected into the original controller, I'm not positive that will do the trick. This article points to the angular docs on $q.

  • Another article recommends using $timeout as what would "actually complete the promise". The article also recommends using "sinon-as-promised," something I'm not doing above. I tried, but didn't see a difference.

  • This Stack Overflow answer use scope.$root.$digest() because "If your scope object's value comes from the promise result, you will need to call scope.$root.$digest()". But again, same test failure. And again, this might be because I'm not using scope.

  • As for stubbing the promise, I also tried the sinon sandbox way, but results were the same.

  • I've tried rewriting the test using $scope, to make sure it's not a problem with the vm style, but the test still fails.

In the end, I could be wrong: the stub and the promise might not be the problem and it's something different and/or obvious that I've missed.

Any help is much appreciated and if I can clarify any of the above, let me know.

jody tate
  • 1,406
  • 1
  • 13
  • 25
  • You say that scope is not injected on the original controller yet you pass it when you instantiate the controller. If your controller does not use injected $scope passing it during test won't make it use it. Most of the time I use `$rootScope.apply()` to trigger promises resolution. Using root scope eliminates the possibility of promises not being triggered – kidroca Jun 10 '17 at 11:13
  • angular's $q service uses $rootScope internally and they suggest to use $rootScope apply - https://docs.angularjs.org/api/ng/service/$q#testing – kidroca Jun 10 '17 at 11:26

2 Answers2

1

Sorry but a quick fix was all that you needed:

var stub = sinon.stub(NavService, 'getNav').returns($q.when({
  response: {
    data: 'data'
  }
}));

Your promise is resolved to object containing response.data not just data Checkout this plunk created from your code: https://plnkr.co/edit/GL1Xuf?p=preview

The extended answer

I have often fallen to the same trap. So I started to define the result returned from a method separately. Then if the method is async I wrap this result in a promise like $q.when(stubbedResult) this allow me to, easily run expectations on the actual result, because I keep the stubbed result in a variable e.g.

it('Controller should have some data', function () {

    var result = {data: 'data'};
    var stub = sinon.stub(NavService, 'getNav').returns($q.when(result));

    var vm = controller(/* initController */);

    scope.$apply();

    stub.callCount.should.equal(1);
    vm.nav.should.equal(result.data)
})

Also some tests debugging skill will come in handy. The easiest thing is to dump some data on the console just to check what's returned somewhere. Working with an actual debugger is preferable of course.

How to quickly catch mistakes like these:

    1. Put a breakpoint at the $rootScope.apply() line (just before it is executed)
    1. Put a breakpoint in the controller's NavService.getNav().then handler to see whether it is called and what it was called with
    1. Continue with the debugger to execute the $rootScope.$apply() line. Now the debugger should hit the breakpoint set at the previous step - that's it.
kidroca
  • 3,480
  • 2
  • 27
  • 44
0

I think you should use chai-as-promised

and then assert from promises like

  doSomethingAsync().should.eventually.equal("foo");

or else use async await

 it('should have some data', async function () {

    await scope.$apply();

  });

you might need to move then getNav() call in init kinda function and then test against that init function

harishr
  • 17,807
  • 9
  • 78
  • 125