AngularJS promises and Jasmine tests

I was working on new component for work, an AngularJS service, that was promised based. Nothing really that special about it. It was using notifications from the deferred API in AngularJS's $q service to send simulated data that would be coming in async via other means. To simulate this, I just send the notification on an interval. Here's the service code:

  var data = [
    {name: "Superman", role: "hero"},
    {name: "Darth Vador", role: "villain"},
    {name: "Spiderman", role: "hero"},
    {name: "Ursula", role: "villain"},
  ];

  return { 
    requestData: function() {

      var i = 0,
          deferred = $q.defer();

      $interval(function () {
          deferred.notify(data[i]);
          i++;
      }, 500, data.length).then(
          function () {
              deferred.resolve('finished');
          }
      );

      return deferred.promise;

    }
  };

And here's a working plunker, if you're so inclined.

In any case, this service is not the reason for this post. It's about how to test it. We're using Jasmine for unit tests, here was the initial test:

    it('should provide myService functionality', function() {
        var items = [];

        expect(myService.requestData).toBeDefined();
        expect(typeof thaliSvc.requestData).toEqual('function');

        myService.requestData().then(
            function onSuccess(result) {
                expect(result).toBeDefined();
                expect(items.length).toBeGreaterThan(0);
            },
            function onError(err) {
                expect(err).toBeUndefined();
            },
            function onNotify(notificationResult) {
                expect(notificationResult).toBeDefined();
                items.push(notificationResult);
            });
    });

It passed. Great. But wait, I just changed my service, which should have caused a failure, and it still passed. What? Turs out Jasmine out of the box can't handle the async nature. The test code runs and moves on. It does not know to wait for the promise to resolve (and thus run the expectations). In fact, no expectations were run.

Enter Jasmine's Async functionality. Add a parameter to the function passed into it, and then can call when your async work is complete. Put this in a finally to make sure it always gets executed when the promise is resolved (or rejected)

    it('should provide myService functionality', function(done) {
           ...
        myService.requestData().then(
            ...
            }).finally(function () {
                done();
            });
    });

That should do it! Nope. I get the error:

Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULTTIMEOUTINTERVAL.

Turns out that this was really just an issue for me since my service was using AngularJS's $interval. As mentioned in the docs

In tests you can use $interval.flush(millis) to move forward by millis milliseconds and trigger any functions scheduled to run in that time.

So at the end of the tests, call that to force $interval to resolve.

    it('should provide myService functionality', function(done) {
           ...
        myService.requestData().then(
            ...
        });

        $interval.flush(500);    
    });

This would not be needed in real world applications, but since I was simulating data being pushed with $interval, it was needed.

Hope this helps! If nothing else, I'll remember these tips for next time...