Testing with Q promises

I just ran into an irritating problem with Q and tests. I do not like asynchronous tests - at all. Firstly, they are hard to read. And secondly, you don't need them. Using a mocking framework like sinon.js, you are able to turn your asynchronous flow into synchronous tests with ease:

The old, bad, and asynchronous way

    describe('Car', function () {
        it('arrives at the destination', function (done) {
            car.driveToDestination(function (err, position) {
                expect(position).to.equal(home);
                done();
            });
            // some code to make the car reach it's destination
        });
    });

Here, the test is as asynchronous as the code. In order to make the tests faster, we are probably going to mock something in the car to make it arrive at it's destination and that will trigger the assertion, some lines above.

The civilised, correct, synchronous way

    describe('Car', function () {
        it('arrives at the destination', function (done) {
            var listener = sinon.spy();
            car.driveToDestination(listener);
            // some code to make the car reach it's destination
            expect(listener).calledWith(null, home);
        });
    });

Here, we use a sinon spy and check the resulting call there. The assertion appears after the mocking that triggers the call. It's so nice and readable!

Enter: Q

Of course we, being so civilised and new, want to use promises to further simplify the control flow of the app. So now we do this:

    describe('Car', function () {
        it('arrives at the destination', function (done) {
            var success = sinon.spy();
            var fail = sinon.spy();
            car.driveToDestination.then(success).catch(fail);
            // some code to make the car reach it's destination
            expect(success).calledWith(home);
            expect(fail).not.called;
        });
    });

...and watch the tests fail. But WHY?

Being somewhat sinon savvy, you realise that Q is probably making sure it's promises are allways resolved and rejected asynchronously by using some kind of timeout magic. So you do this:

    describe('Car', function () {
        var clock;
        beforeEach(function () {
            clock = sinon.useFakeTimers();
        });
        afterEach(function () {
            clock.restore();
        });
        it('arrives at the destination', function (done) {
            var success = sinon.spy();
            var fail = sinon.spy();
            car.driveToDestination.then(success).catch(fail);
            // some code to make the car reach it's destination
            clock.tick();
            expect(success).calledWith(home);
            expect(fail).not.called;
        });
    });

...and watch the tests fail again :(

process.nextTick is the answer

As it turns out, Q uses process.nextTick instead of setTimeout to ensure its asynchronous behaviour in node. So all you have to do is this:

    describe('Car', function () {
        beforeEach(function () {
            sinon.stub(process, 'nextTick').yields();
        });
        afterEach(function () {
            process.nextTick.restore();
        });
        it('arrives at the destination', function (done) {
            var success = sinon.spy();
            var fail = sinon.spy();
            car.driveToDestination.then(success).catch(fail);
            // some code to make the car reach it's destination
            expect(success).calledWith(home);
            expect(fail).not.called;
        });
    });

...and it works! Esentially, what you're doing is replacing process.tick with a sinon stub which immediately yields, i.e calls the passed in function. This effectively turns Q promises synchronous as long as the underlying code responds synchronously and your tests are both green and pretty.

Author

Johan Öbrink

Started programming on my VIC 20 in 1982. Programmed JavaScript, Java, ActionScript, C# and then some. Love TDD, XP and agile development. Enjoy talking (a lot), building drones and playing with LEGO.

comments powered by Disqus