4

I have a controller that expose a function that returns some text after a rest call. It works fine, but I'm having trouble testing it with Jasmine. The code inside the promise handler in the test never executes.

The controller:

/* global Q */
'use strict';
angular.module('myModule', ['some.service'])
    .controller('MyCtrl', ['$scope', 'SomeSvc', function ($scope, SomeSvc) {

        $scope.getTheData = function (id) {
            var deferred = Q.defer();
            var processedResult = '';
            SomeSvc.getData(id)
                .then(function (result) {
                    //process data
                    processedResult = 'some stuff';
                    deferred.resolve(processedResult);
                })
                .fail(function (err) {
                    deferred.reject(err);
                });
            return deferred.promise;
        }
    }]);

The test:

describe('some tests', function() {
    var $scope;
    var $controller;
    var $httpBackend;
    beforeEach(function() {
        module('myModule');
        inject(function(_$rootScope_, _$controller_, _$httpBackend_) {
            $scope = _$rootScope_.$new();
            $controller = _$controller_;
            $httpBackend = _$httpBackend_;
            //mock the call that the SomeSvc call from the controller will make
            $httpBackend.expect('GET', 'the/url/to/my/data');
            $httpBackend.whenGET('the/url/to/my/data')
                .respond({data:'lots of data'});
            $controller ('MyCtrl', {
                $scope: $scope
            });
        });
    });

    describe('test the returned value from the promise', function() {
        var prom = $scope.getTheData(someId);
        prom.then(function(result) {
            expect(result).toBe('something expected');  //this code never runs
        })
    });
});
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
binarygiant
  • 6,362
  • 10
  • 50
  • 73
  • What are the symptoms? Any errors? – alecxe Oct 11 '14 at 22:39
  • @alecxe the symptoms are that the code inside the then never runs, so I cant make an assertion. Updated the code and question to better reflect the issue. Thank you. – binarygiant Oct 11 '14 at 23:04
  • 1
    Any reason why you're using `Q` and not `$q`? And what sort of promise does `SomeSvc.getData` return: one created using `$q` (such as those returned from `$http`), or one from `Q`)? – Michal Charemza Oct 12 '14 at 07:37
  • This project uses big Q rather than the provided $q with Angular, because $q was missing something that Q provided. The design decision was made a while back, and I dont recall the details at the moment. Both promises in question return a promise created by big Q. – binarygiant Oct 12 '14 at 12:34
  • Looks like you use the [Deferred antipattern](https://stackoverflow.com/questions/23803743/what-is-the-deferred-antipattern-and-how-do-i-avoid-it) (although hardly the reason for your problems) – Bergi Oct 12 '14 at 15:05
  • Did any of the answers help you? – oldwizard Nov 04 '14 at 11:48
  • This could have had such an easy answer: https://jasmine.github.io/2.0/introduction.html#section-Asynchronous_Support – oligofren Jun 11 '17 at 07:40

3 Answers3

9

Anything inside a then will not be run unless the promise callbacks are called - which is a risk for a false positive like you experienced here. The test will pass here since the expect was never run.

There are many ways to make sure you don't get a false positive like this. Examples:

A) Return the promise

Jasmine will wait for the promise to be resolved within the timeout.

  • If it is not resolved in time, the test will fail.
  • If the promise is rejected, the test will also fail.

Beware If you forget the return, your test will give a false positive!

describe('test the returned value from the promise', function() {
    return $scope.getTheData(someId)
    .then(function(result) {
        expect(result).toBe('something expected');
    });
});

B) Use the done callback provided by Jasmine to the test method

  • If done is not called within the timeout the test will fail.
  • If done is called with arguments the test will fail.
    The catch here will pass the error to jasmine and you will see the error in the output.

Beware If you forget the catch, your error will be swallowed and your test will fail with a generic timeout error.

describe('test the returned value from the promise', function(done) {
    $scope.getTheData(someId)
    .then(function(result) {
        expect(result).toBe('something expected');
        done();
    })
    .catch(done);
});

C) Using spies and hand cranking (synchronous testing)

If you're not perfect this might be the safest way to write tests.

it('test the returned value from the promise', function() {
    var
      data = { data: 'lots of data' },
      successSpy = jasmine.createSpy('success'),
      failureSpy = jasmine.createSpy('failure');

    $scope.getTheData(someId).then(successSpy, failureSpy);

    $httpBackend.expect('GET', 'the/url/to/my/data').respond(200, data);
    $httpBackend.flush();

    expect(successSpy).toHaveBeenCalledWith(data);
    expect(failureSpy).not.toHaveBeenCalled();
});

Synchronous testing tricks
You can hand crank httpBackend, timeouts and changes to scope when needed to get the controller/services to go one step further. $httpBackend.flush(), $timeout.flush(), scope.$apply().

oldwizard
  • 5,012
  • 2
  • 31
  • 32
  • `. So don't use expect inside then in tests.` That is terrible advise. Learn to use your frameworks general support for testing async code instead. Jasmine: https://jasmine.github.io/2.0/introduction.html#section-Asynchronous_Support Mocha: https://mochajs.org/#asynchronous-code – oligofren Jun 11 '17 at 07:38
  • Your comment isn't informative. If you have a better answer, answer the question with your own answer. The Jasmine docs specifically do not use ".then" on any of it's tests. The answer was provided in 2014, perhaps something has changed. I still believe it is a really bad idea to put expects in ".then". Prove me wrong. – oldwizard Jun 13 '17 at 07:44
  • Not informative? I linked directly to the doc sections on async testing of the two most popular test frameworks to show how it's done. The code you are testing is async, yet you just provide synchronous imperatives. The point here is that Promises belong to the async dimension, so even though the docs does not use promises (or generators, async/await, etc ...) it shows how to do async test programming. That involves passing the `done` parameter to the async part of the test. In promises, that is in the callback to a `Thenable`. As in: `doSomething().then(done).catch(done)`. – oligofren Jun 13 '17 at 08:04
  • I look forward to all StackOverflow answers being replaced with links to docs mate. ;) It is easy to write the test with cleaner code, everyone learns over the years, but not by reading the Jasmine docs you linked to, they're useless in this case imo. – oldwizard Jun 13 '17 at 09:41
  • Well whatever the case, your answer is much better now :-) Upvoted. – oligofren Jun 13 '17 at 14:44
1

In case there is a promise created by $q somewhere (and since you seem to be using $httpBackend, then this might well be the case), then my suggestions are to trigger a digest cycle after the call to getTheData, and make sure that the call to expect isn't in a then callback (so that if something is broken, the test fails, rather than just not run).

var prom = $scope.getTheData(someId);
$scope.$apply();
expect(result).toBe('something expected');

The reason this might help is that Angular promises are tied to the digest cycle, and their callbacks are only called if a digest cycle runs.

Michal Charemza
  • 25,940
  • 14
  • 98
  • 165
0

The problem you face is that you are testing async code (promise based) using synchronous code. That won't work, as the test has finished before the Thenable in your test code has started running the callback containing the test. The Jasmine docs specifically show how to test async code (same as Mocha), and the fix is very small:

describe('test the returned value from the promise', function(done) {
    var prom = $scope.getTheData(someId);
    prom.then(function(result) {
        expect(result).toBe('something expected');  //this code never runs
        done(); // the signifies the test was successful
    }).catch(done); // catches any errors
});
oligofren
  • 20,744
  • 16
  • 93
  • 180
  • I don't believe jasmine made this available at the time the question was posted. And/or the project was not able to bumpthe jasmine version that did allow for "done". – binarygiant Jun 14 '17 at 17:11
  • @binarygiant Since asynchronicity is such a big part of every javascript project Jasmine has of course had support for async testing since the start :-) Jasmine 1.0 used `runs`, `waits` and `waitsFor`, while Jasmine 2.0 (introduced in 2013) aligned with Mocha in just using a callback to accomplish the same. See https://jasmine.github.io/2.0/upgrading.html – oligofren Jun 15 '17 at 08:10
  • Yup, that's great. The question was answered several years ago. Are we flexing our condescending muscles here? – binarygiant Jun 16 '17 at 14:37
  • Jeesus, man, no need to get your panties in a bunch! I was not trying to be condescending, and if it came out like that I apologize. This is the top Q&A site just by the pure fact that answers are continually refined. The accepted was incorrect, and so I added a correct one. The accepted one has now been improved. This is practice is _encouraged_, as people still find older questions when googling. – oligofren Jun 16 '17 at 16:26
  • Thank you for your answer. You do sound condescending and arrogant. Work on that. – binarygiant Jun 16 '17 at 17:17
  • Thanks for nothing. – oligofren Jun 16 '17 at 17:39