unit testing angularjs $q.all - promise never completes
Asked Answered
V

2

8

I'm trying to test a service that I build which uses Angular's $q implementation of Promises. I'm using a combination of Karma, Mocha, Chai, Sinon, Sinon Chai and Chai as Promised.

All the tests that I wrote and return promises are passing but the ones that reject or uses $q.all([ ... ]). I have tried all I could think of but I cannot seem to find where the issue is.

The following is a slim version of what I am testing:

"use strict";


describe("Promise", function () {

    var $rootScope,
        $scope,
        $q;

    beforeEach(angular.mock.inject(function (_$rootScope_, _$q_) {
        $rootScope = _$rootScope_;
        $q = _$q_;
        $scope = $rootScope.$new();
    }));

    afterEach(function () {
        $scope.$apply();
    });

    it("should resolve promise and eventually return", function () {

        var defer = $q.defer();

        defer.resolve("incredible, this doesn't work at all");

        return defer.promise.should.eventually.deep.equal("incredible, this doesn't work at all");
    });

    it("should resolve promises as expected", function () {

        var fst = $q.defer(),
            snd = $q.defer();

        fst
            .promise
            .then(function (value) {
                value.should.eql("phew, this works");
            });

        snd
            .promise
            .then(function (value) {
                value.should.eql("wow, this works as well");
            });

        fst.resolve("phew, this works");
        snd.resolve("wow, this works as well");

        var all = $q.all([
            fst.promise,
            snd.promise
        ]);

        return all.should.be.fullfiled;
    });

    it("should reject promise and eventually return", function () {
        return $q.reject("no way, this doesn't work either?").should.eventually.deep.equal("no way, this doesn't work either?");
    });

    it("should reject promises as expected", function () {

        var promise = $q.reject("sadly I failed for some stupid reason");

        promise
            ["catch"](function (reason) {
                reason.should.eql("sadly I failed for some stupid reason");
            });

        var all = $q.all([
            promise
        ]);

        return all.should.be.rejected;
    });

});

The 3rd, last and the first test are the ones that fail. Actually it does not fail, it just resolves after the timeout is exceeded and I get a Error: timeout of 2000ms exceeded.

EDIT: I have just tried to test with Kris Kowal's implementation of the promises and it works just fine with that.

P.S. I actually found that there is some time spent somewhere in the bowls of either Mocha, Chai or Chai As Promised and the afterEach hook gets called later than the timeout.

Vincenza answered 5/11, 2014 at 7:33 Comment(4)
If I'm understanding the pseudo-code, it looks like you're not calling $scope.$apply()until after you text the expectations. Can you try calling it just after you resolve/reject the promise?Purificator
I actually have tried that as well, I'll have a fiddle up and running soon with these tests.Vincenza
Running $scope.$apply() in afterEach could be an issue in case you expect the value of the promises before the afterEach is runNoontime
@khanhto - that is true, and @proloser mentioned that I use afterEach for cleanup only. It just didn't occur to me at the point I did it.Vincenza
V
3

I've tried to find out why the tests are not passing even though at first glance they should. Of course I would have to move the $scope.$apply(); from afterEach since that is not the place to call as @proloser mentioned.

Even though I have done that, the tests are still not passing. I've also opened issues on chai-as-promised and angular to see if I get any input/feedback and I've actually been told that it's most likely not to work. The reason is probably because of Angular $q's dependency on the digest phase which is not accounted for in the chai-as-promsied library.

Therefore I've checked the tests with Q instead of $q and it worked just fine, thus strengthening my hypothesis that the fault was not in the chai-as-promised library.

I've eventually dropped chai-as-promised and I've rewritten my test using Mocha's done callback instead (even though behind the scenes, chai-as-promised does the same):

"use strict";


describe("Promise", function () {

    var $rootScope,
        $scope,
        $q;

    beforeEach(angular.mock.inject(function (_$rootScope_, _$q_) {
        $rootScope = _$rootScope_;
        $q = _$q_;
        $scope = $rootScope.$new();
    }));

    it("should resolve promise and eventually return", function (done) {

        var defer = $q.defer();

        defer
            .promise
            .then(function (value) {
                value.should.eql("incredible, this doesn't work at all");
                done();
            });

        defer.resolve("incredible, this doesn't work at all");

        $scope.$apply();

    });

    it("should resolve promises as expected", function (done) {

        var fst = $q.defer(),
            snd = $q.defer();

        fst
            .promise
            .then(function (value) {
                value.should.eql("phew, this works");
            });

        snd
            .promise
            .then(function (value) {
                value.should.eql("wow, this works as well");
            });

        fst.resolve("phew, this works");
        snd.resolve("wow, this works as well");

        var all = $q.all([
            fst.promise,
            snd.promise
        ]);

        all
            .then(function () {
                done();
            });

        $scope.$apply();

    });

    it("should reject promise and eventually return", function (done) {

        $q
            .reject("no way, this doesn't work either?")
            .catch(function (value) {
                value.should.eql("no way, this doesn't work either?");
                done();
            });

        $scope.$apply();

    });

    it("should reject promises as expected", function (done) {

        var promise = $q.reject("sadly I failed for some stupid reason");

        promise
            ["catch"](function (reason) {
                reason.should.eql("sadly I failed for some stupid reason");
            });

        var all = $q.all([
            promise
        ]);

        all
            .catch(function () {
                done();
            });

        $scope.$apply();

    });

});

The above tests will all pass as expected. There might be other ways to do it, but I could not figure out how else, so if anyone else does, it would be great to have it posted so others can benefit from it.

Vincenza answered 14/11, 2014 at 8:13 Comment(1)
@blacksonic - no problem, I spent days myself until I found a fix :)Vincenza
T
12

afterEach() is used for cleanup, not for executing code after your preparations but before your tests. $scope.$apply() is not cleanup either.

You need to be doing the following:

// setup async behavior
var all = $q.all(x.promise, y.promise)

// resolve your deferreds/promises
x.reject(); y.reject();

// call $scope.$apply() to 'digest' all the promises
$scope.$apply();

// test the results
return all.should.be.rejected;

You're doing an $apply() AFTER your tests are done, not in between setup and evaluation.

Thighbone answered 9/11, 2014 at 7:30 Comment(3)
If you'd like you can try to test it on this plnk, which I have a hard time setting it up. I think one of my resources is invalid or something, because I get an error in the console which tells that I'm missing the beforeEach. I've never tried to test in the browser, so some help getting it to work would be appreciated so we can see if this answer actually works and we'll also have an online reference so other users can see it.Vincenza
That is what I wrote in the comment, that some help would be appreciated setting it up in the browser as I never did it that way. And you could just make a plnkr and apply your fix and show that it actually works, if you want to of course. Doing it locally on my machine and saying that it works it will not help other users that stumble upon the same issue.Vincenza
your suggestion does not work, and I've already tried what you suggested before. That is why I asked if you can help setting up the tests in the browser so you can check it as well and other users if it would have worked.Vincenza
V
3

I've tried to find out why the tests are not passing even though at first glance they should. Of course I would have to move the $scope.$apply(); from afterEach since that is not the place to call as @proloser mentioned.

Even though I have done that, the tests are still not passing. I've also opened issues on chai-as-promised and angular to see if I get any input/feedback and I've actually been told that it's most likely not to work. The reason is probably because of Angular $q's dependency on the digest phase which is not accounted for in the chai-as-promsied library.

Therefore I've checked the tests with Q instead of $q and it worked just fine, thus strengthening my hypothesis that the fault was not in the chai-as-promised library.

I've eventually dropped chai-as-promised and I've rewritten my test using Mocha's done callback instead (even though behind the scenes, chai-as-promised does the same):

"use strict";


describe("Promise", function () {

    var $rootScope,
        $scope,
        $q;

    beforeEach(angular.mock.inject(function (_$rootScope_, _$q_) {
        $rootScope = _$rootScope_;
        $q = _$q_;
        $scope = $rootScope.$new();
    }));

    it("should resolve promise and eventually return", function (done) {

        var defer = $q.defer();

        defer
            .promise
            .then(function (value) {
                value.should.eql("incredible, this doesn't work at all");
                done();
            });

        defer.resolve("incredible, this doesn't work at all");

        $scope.$apply();

    });

    it("should resolve promises as expected", function (done) {

        var fst = $q.defer(),
            snd = $q.defer();

        fst
            .promise
            .then(function (value) {
                value.should.eql("phew, this works");
            });

        snd
            .promise
            .then(function (value) {
                value.should.eql("wow, this works as well");
            });

        fst.resolve("phew, this works");
        snd.resolve("wow, this works as well");

        var all = $q.all([
            fst.promise,
            snd.promise
        ]);

        all
            .then(function () {
                done();
            });

        $scope.$apply();

    });

    it("should reject promise and eventually return", function (done) {

        $q
            .reject("no way, this doesn't work either?")
            .catch(function (value) {
                value.should.eql("no way, this doesn't work either?");
                done();
            });

        $scope.$apply();

    });

    it("should reject promises as expected", function (done) {

        var promise = $q.reject("sadly I failed for some stupid reason");

        promise
            ["catch"](function (reason) {
                reason.should.eql("sadly I failed for some stupid reason");
            });

        var all = $q.all([
            promise
        ]);

        all
            .catch(function () {
                done();
            });

        $scope.$apply();

    });

});

The above tests will all pass as expected. There might be other ways to do it, but I could not figure out how else, so if anyone else does, it would be great to have it posted so others can benefit from it.

Vincenza answered 14/11, 2014 at 8:13 Comment(1)
@blacksonic - no problem, I spent days myself until I found a fix :)Vincenza

© 2022 - 2024 — McMap. All rights reserved.