0

I'm attempting to test that a value is changed to true after a promise is resolved inside $onInit. I'm following, as best I can, the example in this Stack Overflow question/answer. Here is my code:

class TestCtrl {
  constructor(SearchService) {
    this.testValue = false;
    this.SearchService = SearchService;
  }
  $onInit() {
    this.SearchService.getResults()
      .then(function () {
        this.testValue = true;
      });
  }
}
TestCtrl.$inject = ['SearchService'];

And here's the test I'm attempting to run (using mocha, chai, sinon):

it('should work', function() {
  ctrl = $componentController('test', {
    SearchService: SearchService
  }, {});
  sinon.stub(SearchService, 'getResults').resolves({response:{data: 'data'}});
  ctrl.$onInit();
  $rootScope.$apply();
  ctrl.testValue.should.equal(true);
});

Should I be testing ctrl.testValue inside a then? Also, is using this example a bad idea because that example doesn't use a component with an $onInit lifecycle hook?

From what I've read, no, "don't use expect inside then in tests." But I'm not so sure based on what I've read elsewhere.

I wouldn't be surprised if I'm missing something obvious in how to test promises (maybe a stub wasn't the way to go?) and/or how to test what happens in the $onInit lifecycle hook.

If the question needs more details, please ask and I'll do my best to add them.

oligofren
  • 20,744
  • 16
  • 93
  • 180
jody tate
  • 1,406
  • 1
  • 13
  • 25
  • 2
    There is no *general reason* you should not use `expect` inside `.then`. The OP in the question you linked to was using Jasmine, not Mocha. Maybe Jasmine sucks at dealing with promises (this would then be a *particular* reason to not use `expect` in `then`, not a *general* one), but Mocha certainly does not suck at dealing with promises. You just need to remember to return the promise. – Louis Jun 08 '17 at 10:53
  • Why do you need to call `$rootScope.apply()`? – oligofren Jun 11 '17 at 07:24
  • 1
    Just to elaborate on Louis' answer: both Mocha and Jasmine can handle any type of async code (including Promises) by having the framework pass in a callback to the test function that the test code calls when the test successfully completes. Mocha has some special handling that also allows it to drop the callback by returning the promise, which makes for somewhat smoother testing (see details below). – oligofren Jun 11 '17 at 12:33
  • you are using `this` in an anonymous function in your `then` handler. `this.testValue = true;` will be assigned to the `window` and not your instance of `TestCtrl`. In order for `this` to refer to `TestCtrl` instance you should use an *arrow function* if possible, or wrap `this` in variable like `var ctrl = this` – kidroca Jun 14 '17 at 13:32

3 Answers3

2

Edit: Checkout you $onInit method:

$onInit() {
    this.SearchService.getResults() 
      .then(function () {
         // `this` in anonymous function is reffering to window not the controller instance
        this.testValue = true;
      });
  }

$onInit() {
    var self = this;
    self.SearchService.getResults() 
      .then(function () {
        self.testValue = true;
      });
  }

Your example is correct

This is the way to test async code in angularjs - it is tested like synchronous code. Stubs' returning promises are resolved when you execute $rootScope.$apply().

Why it doesn't work

The promise returned from stub.resolves() is not an angular promise. It cannot be triggered to resolve using $rootScope, because it's not a part of angular's world. It's promise resolution queue is tied to something else and hence the need to test like you usually test async code.

Angular doesn't depend on JavaScript's native Promise implementation - it uses a light implementation of Q's promise library that is wrapped in a service called $q

The answer you have quoted uses the same service to create and return a promise from a stub

In order for your code to work - to test like you test synchronous code - you should return a $q promise (By wrapping a value in $q.when(value)) calling $rootScope.$apply() will execute the code in the then block, then proceed with the code below $rootScope.$apply() line.

Here is an example:

it('Sinon should work with angular promises', function () {

    var resolved = 'resolved';
    var promise = $q.when(resolved);
    // Our async function
    var stub = sinon.stub().returns(promise);
    // Callback to be executed after the promise resolves
    var handler = sinon.stub(); 

    stub().then(handler); // async code

    // The handler will be called only after $rootScope.$apply()
    handler.callCount.should.equal(0);

    // triggers digest which will resolve `ready` promises 
    // like those created with $q.when(), $q.resolve() or those created 
    // using the $q.defer() and deferred.resolve() was called
    // this will execute the code inside the appropriate callback for
    // `then/catch/finally` for all promises and then continue 
    // with the code bellow -> this is why the test is considered `synchronous`
    $rootScope.$apply();

    // Verify the handler was called and with the expected value
    handler.callCount.should.equal(1);
    handler.should.have.been.calledWith(resolved);

})

Here it is in action test promise synchronously in angular

kidroca
  • 3,480
  • 2
  • 27
  • 44
1

For starters, you should read up on how Mocha expects you to test async code.

To start out with the quick bits:

  1. You are on the right path - there are just some bits missing.
  2. Yes you should do your test inside a then.
  3. The example you linked to is fine. Just understand it.
  4. There is absolutely no reason to avoid asserting a test inside a then. In fact, it is usually the only way to assert the resolved value of a promise.

The main problem with your test code is it tries to assert the result before it is available (as promises resolve in a later tick, they are asynchronous).

The main problem with the code you are trying to test is that there is no way of knowing when the init function has resolved.

We can deal with #2 by waiting for the stubbed SearchService.getResults to resolve (as we control the stub in the test), but that assumes too much knowledge of the implementation of onInit, so that is a bad hack.

Instead, we fix the code in TestCtrl, simply by returning the promise in onInit:

//main code / TestCtrl
$onInit() {
  return this.SearchService.getResults()
    .then(function () {
      this.testValue = true;
    }); 
}

Now we can simply wait for any call to onInit to resolve before we test what has happened during its execution!

To fix your test we first add a parameter to the wrapping test function. Mocha will see this and pass in a function that you can call when your test finishes.

it('should work', function(done) {

That makes it an async test. Now lets fix the test part:

ctrl.$onInit().then( () => {
  ctrl.testValue.should.equal(true);
  done(); // signals to mocha that the test is finished ok
}).catch(done); // pass any errors to the callback

You might find also find this answer enlightening (upvote if it helps you out). After reading it you might also understand why Mocha also supports dropping the done callback by returning a promise from the test instead. Makes for shorter tests:

return ctrl.$onInit().then( () => {
  ctrl.testValue.should.equal(true);
});
oligofren
  • 20,744
  • 16
  • 93
  • 180
-1

sinon.stub(SearchService, 'getResults').resolves({response:{data: 'data'}}); is not returning a promise. Use $q. I would suggest doing this:

ctrl = $componentController('test', {
    SearchService: SearchService
}, {});
let deferred =$q.defer();
deferred.resolve({response:{data: 'data'}});
sinon.stub(SearchService, 'getResults').resolves(deferred.promise);
ctrl.$onInit();
$rootScope.$apply();
ctrl.testValue.should.equal(true);

You don't need to test ctrl.testValue inside a then. And generally, I would recommend not assert inside .then() in your specs. The specs will not fail if the promise never gets resolved. That can give you a false sense of security when in reality, your tests are not doing anything. But that's just my opinion. Your test will pass once the stub returns a promise. Ideally, I would recommend using $httpBackend if the service is making an http call. Cheers.

oligofren
  • 20,744
  • 16
  • 93
  • 180
Spitfire
  • 147
  • 1
  • 5
  • 1
    Your first sentence is incorrect. `stub.resolves()` does return a promise. That's the whole point of that feature. Easy promise stubbing. Try this: `stub = sinon.stub().resolves(42); stub().then(res => console.log(res) );` You might be thinking of `stub.returns`. – oligofren Jun 11 '17 at 06:55
  • 1
    Also, your test does not deal with the main problem: async code tested using sync principles is invalid. Read up on how to test async code using Mocha: https://mochajs.org/#asynchronous-code – oligofren Jun 11 '17 at 06:59
  • 1
    It is also generally incorrect to state "You don't need to test ctrl.testValue inside a then" when there is a promise that changes the value in another tick. You can of course trigger a test to run using `setTimeout` to the same effect, but you will need to guess at the time required. – oligofren Jun 11 '17 at 07:35