justinhelmer
12/29/2015 - 5:30 AM

A flatter pattern for testing nested async functions

A flatter pattern for testing nested async functions

// http://jsfiddle.net/1mjzpapb/
var controller, helpers = window.unitTestHelpers;

// controller.js
(function () {

  controller = {
    async1: forever,
    async2: forever,
    async3: forever,

    sync1: function () {},
    sync2: function () {},
    sync3: function () {},

    someAsyncMethod: someAsyncMethod
  };

  function someAsyncMethod() {

    /**/
    return controller
      .async1('foo')
      .then(function (result) {
        controller.sync1(result);
        return controller.async2('bar');
      })
      .then(function (result) {
        controller.sync2(result);
        return controller.async3('baz');
      })
      .then(function (result) {
        controller.sync3(result);
        return result;
      });
    /**/
    
    /** Alternatively written as:
    return controller.async1('foo').then(function (result) {
      controller.sync1(result);

      return controller.async2('bar').then(function (result) {
        controller.sync2(result);

        return controller.async3('baz').then(function (result) {
          controller.sync3(result);
          return result;
        });
      });
    });
    /**/
  }
})();

// controller.spec.js
(function () {
  describe('controller', function () {
    describe('#someAsyncMethod', function () {
      var promiseResponse;
      
      beforeEach(function (done) {
        helpers.spies.extendObj(controller, ['sync1', 'sync2', 'sync3',
          { name: 'async1', and: { returnValue: 'test1' }, options: { promise: true }},
          { name: 'async2', and: { returnValue: 'test2' }, options: { promise: true }},
          { name: 'async3', and: { returnValue: 'test3' }, options: { promise: true }}
        ]);

        promiseResponse = controller.someAsyncMethod();
        Q.all([controller.async1, controller.async2, controller.async3]).done(done);
      });
      
      it('should call async1', function () {
        expect(controller.async1).toHaveBeenCalledWith('foo');
      });
      
      it('should return the value of async3', function (done) {
      	promiseResponse.then(function (result) {
          expect(result).toBe('test3');
          done();
        });
      });

      it('#async1 should call sync1', function () {
        expect(controller.sync1).toHaveBeenCalledWith('test1');
      });
      
      it('#async1 should call async2', function () {
        expect(controller.async2).toHaveBeenCalledWith('bar');
      });
      
      it('#async2 should call sync2', function () {
        expect(controller.sync2).toHaveBeenCalledWith('test2');
      });
      
      it('#async2 should call async3', function () {
        expect(controller.async3).toHaveBeenCalledWith('baz');
      });
      
      it('#async3 should call sync3', function () {
        expect(controller.sync3).toHaveBeenCalledWith('test3');
      });
    });
  });
})();

function forever() {
  var dfd = Q.defer();

  setTimeout(function () {
    dfd.resolve();
  }, 10000000000)

  return dfd.promise;
}