16

I am trying to unit test a service which has asynchronous methods but am having no luck.

I have tried to implement with promises by using the $q support in angularjs.

Any help would be appreciated.

http://jsfiddle.net/9pBze/37/

angular.module('myapp', ['myservice']);

angular.module('myservice', []).factory('myservice', function($q) {
  var ls = {};

  ls.DoIt = function() {
    var deferred = $q.defer();

    setTimeout(function(){
        deferred.resolve(5);
    },3000);

    return deferred.promise;
  }

  return ls;

});

describe('services', function () {

    beforeEach(module('myservice'));

    it("should equal 2",  inject(function(myservice) {
        myservice.DoIt().then(function(returned) {
            expect(returned).toEqual(2);
        });        
    }));
});
Cheesebaron
  • 24,131
  • 15
  • 66
  • 118
Hidden Developer
  • 161
  • 1
  • 1
  • 3

2 Answers2

30

First of all, the setTimeout is particularly tricky to test since it hard to mock. Fortunately AngularJS has a wrapper around it ($timeout) that plays the same role but can be easily mocked:

  ls.DoIt = function() {
    var deferred = $q.defer();

    $timeout(function(){
        deferred.resolve(5);
    },3000);

    return deferred.promise;
  }

The mock provided for $timeout allows us to easily simulate elapsed time (with $timeout.flush()) which means our tests can run fast, without really waiting for the async event to complete (please note that the production code is still using async API!).

The changed tests would look like:

it("should equal 5",  inject(function(myservice, $timeout) {

    var valueToVerify;
    myservice.DoIt().then(function(returned) {
      valueToVerify = returned;  
    });  
    $timeout.flush();        
    expect(valueToVerify).toEqual(5);
}));

And finally the working jsFiddle: http://jsfiddle.net/v9L9G/1/

Mark Amery
  • 143,130
  • 81
  • 406
  • 459
pkozlowski.opensource
  • 117,202
  • 60
  • 326
  • 286
  • Thanks for the detailed explanation. The service I am writing uses IndexedDB and the api is asynchronous and I am struggling as to how I can apply the above when the DoIt function would call something like request.onsuccess = function(event) { // Do something with request.result! deferred.resolve(result); }; – Hidden Developer Mar 19 '13 at 00:35
  • @Pawel What if I'm testing a controller which uses `DoIt` mock service, and inside that controller, `DoIt` is in a loop, so it gets called multiple times. How do I flush it multiple times? Isn't it possible to get `DoIt` to flush itself? I tried to get an asynchronous mock to flush itself by including [this](http://jsfiddle.net/cUEQ8/) in it, but that doesn't seem very robust. – M.K. Safi Sep 26 '13 at 14:28
  • Actually, the fiddle I shared above seems to successfully get a mock to flush itself. The problem I was having turned out to be some where else... – M.K. Safi Sep 26 '13 at 15:20
  • @pkozlowski.opensource How do we test that `valueToVerify` was still undefined after 1000ms? – Lukasz Prus Oct 01 '14 at 19:01
4

It's not related to Angular itself, but to Jasmine async tests.

If you need a setTimeout use Angular $timeout. And if you wish to have a fine control over setTimeout/$timeout executions, use mocked Clock.

Caio Cunha
  • 23,326
  • 6
  • 78
  • 74