2
votes

I have an AngularJS app where I need to test a workflow and guarantee that the correct values are set after an event is broadcasted.

In 1.3 I would do this:

it('should have the correct match workflow', function() {
    // matchMaking event
    runs(function() {
        scope.$broadcast('matchMaking', gameId);
    });

    waitsFor(function() {
        return (scope.match && scope.match.game);
    }, 'A game should be defined', 3000);

    runs(function() {
        expect(scope.match.game).toBeDefined();
    });

    // matchCreate event
    runs(function() {
        scope.$broadcast('matchCreate', gameId, {}, {});
    });

    waitsFor(function() {
        return scope.match.status === 'CREATED';
    }, 'Match status should be \'CREATED\'', 3000);

    runs(function() {
        expect(scope.match.id).toBeDefined();
        expect(scope.match.player).toBeDefined();
        expect(scope.match.opponent).toBeDefined();
    });

    // matchPrepare event
    runs(function() {
        scope.$broadcast('matchPrepare');
    });

    waitsFor(function() {
        return scope.match.status === 'PREPARED';
    }, 'Match status should be \'PREPARED\'', 3000);

    runs(function() {
        expect(scope.match.id).toBeDefined();
    });

    // ... continues
});

With Jasmine 2.0, it seems that the only solution to test a workflow is to chain setTimeout functions inside each other (all expectations must be inside the same spec in order to use the same scope):

beforeEach(inject(function($rootScope, $compile) {
    jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000;
    scope = $rootScope;
    element = angular.element('<pg-match-making></pg-match-making>');
    $compile(element)($rootScope);
    $rootScope.$digest();
}));

it('should have the correct match workflow', function(done) {
    var timeoutTick = 100;
    scope.$broadcast('matchMaking', gameId);
    setTimeout(function(){
        expect(scope.match.game).toBeDefined();

        scope.$broadcast('matchCreate', gameId, {}, {});
        setTimeout(function(){
            expect(scope.match.status).toEqual('CREATED');
            expect(scope.match.id).toBeDefined();
            expect(scope.match.player).toBeDefined();
            expect(scope.match.opponent).toBeDefined();

            scope.$broadcast('matchPrepare');
            setTimeout(function(){
                expect(scope.match.status).toEqual('PREPARED');
                expect(scope.match.id).toBeDefined();

                // ... call done() on the last setTimeout()
            }, timeoutTick);
        }, timeoutTick);
    }, 6000);
});

I ended up with a pile of 7 setTimeout which make the source code a lot harder to read and the test terribly slow to run.

Isn't there a better way to test a workflow with Jasmine 2.0?

2
Did you ever find a more elegant solution to this? I'm currently moving to jasmine 2.2 from 1.3.1 and I have the same problemFrederik
Unfortunately I have not.Samuel Santos

2 Answers

1
votes

I have a solution for your problem. I have build a small simple async test framework that works well with Jasmine 2.x, but it uses the jQuery Deferred object to schedule continuations.

function asyncWait(delay) {
    return new $.Deferred(function () {
        var _self = this;
        setTimeout(function () {
            _self.resolve();
        }, delay || 0);
    }).promise();
}

var Async = function(init) {
    var d = new $.Deferred(init);
    this.promise = d.promise();
    d.resolve();
};

Async.prototype.continueWith = function (continuation, delay) {
    var _self = this;
    _self.promise.then(function () {
        _self.promise = asyncWait(delay).then(continuation);
    });
    return _self;
};

Async.prototype.waitsFor = function (condition, timeout, pollInterval) {
    pollInterval = pollInterval || 10;
    timeout = timeout || 5000;
    var _self = this,
        wait_d = new $.Deferred(),
        t = 0,
        ln = function () {
            if (condition()) {
                wait_d.resolve();
                return;
            }
            if (t >= timeout) {
                wait_d.reject();
                throw "timeout was reached during waitsFor";
            }
            t += pollInterval;
            setTimeout(ln, pollInterval);
        };
    _self.promise.then(ln);
    _self.promise = wait_d.promise();
    return _self;
};

To use this code, wire up a Jasmine test and use a new instance of the Async class,

    it("some async test workflow I want to run", function (done) {
    new Async(function () {
        //wire up the first async call here
        var timeoutTick = 100;
        scope.$broadcast('matchMaking', gameId);
    }).continueWith(function () {
        expect(scope.match.game).toBeDefined();
        scope.$broadcast('matchCreate', gameId, {}, {})
    }, 6000).continueWith(function () {
        //more stuff here
    }).waitsFor(function () {
        // a latch function with timeout - maybe wait for DOM update or something
        return $(".my-statefull-element").val() === "updated";
    }, 1000).continueWith(done); //finish by waiting for done to be called
});

This code is not a 100% fool proof, but it works for me. Let me know if you have any issues with it.

1
votes

With a little bit of extra javascript, you can make the jasmine behave similarly to what you had with 1.3.1, and you don't need to pull in any additional libraries. You just need to implement the polling function that you are missing. Here's a simplified example:

var value1 = false;
var value2 = false;
var value3 = false;

var test1 = function _test1() {
    setTimeout( function() { value1 = true; }, 1000 );
}

var test2 = function _test2() {
    setTimeout( function() { value2 = true; }, 5000 );
}

var test3 = function _test3() {
    setTimeout( function() { value3 = true; }, 300000 );
}   

var asyncCheckFn = function( done, waitFor, verify ) {
    if ( waitFor() ) {
        verify();
        done();
    } else {
        console.log( 'checking...' );
        setTimeout( function() { asyncCheckFn(done, waitFor, verify) }, 500);
    }   
};      

describe('async test suite', function() {

    it( 'works with short test', function( done ) {
        test1(); 
        asyncCheckFn( done, function() {
           return value1;
        }, function() {
           expect( value1 ).toBe( true );
        }); 
    }, 3000 );


    it( 'longer delay', function( done ) {
        test2();
        asyncCheckFn( done, function() {
           return value2;
        }, function() {
           expect( value2 ).toBe( true );
        });
    }, 10000 );


    it( 'fails', function( done ) {
        test3();
        asyncCheckFn( done, function() {
            return value3;
        }, function() {
           expect( value3 ).toBe( true );
        });
    }, 3000 );

});

The asyncTestFn() performs the same task that the waitsFor() function used to do -- tests a condition until it is true. The overall timeout for the test is controlled by the last parameter passed to the it() function. Here's your example rewritten as a linear test instead of nested setTimeouts:

describe('should have the correct match workflow', function() {

  var timerTick = 100;

  // matchMaking event
  it('defines the game', function(done) {
    scope.$broadcast('matchMaking', gameId);
    asyncCheckFn(done, function() {
      return scope.match && scope.match.game;
    }, function() {
      expect(scope.match.game).toBeDefined();
    });
  }, 6000);


  it('creates the match', function(done) {
    scope.$broadcast('matchCreate', gameId, {}, {});
    asyncCheckFn(done, function() {
      return scope.match.status === 'CREATED';
    }, function() {
      expect(scope.match.id).toBeDefined();
      expect(scope.match.player).toBeDefined();
      expect(scope.match.opponent).toBeDefined();
    });
  }, timerTick);


  it('prepares the match', function(done) {
    scope.$broadcast('matchPrepare');
    asyncCheckFn(done, function() {
      return scope.match.status === 'PREPARED';
    }, function() {
      expect(scope.match.id).toBeDefined();
    });
  }, timerTick);

  // ... continues
});

Hope this helps.

(I know this is a little old, but I came across the question when trying to solve a similar problem -- how to nest sequential, dependent tests (answer, you can't... ))

(samples tested with Jasmine 2.2)