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?
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.
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)
Related
So I created the following mixin:
var Polling = {
startPolling: function() {
var self = this;
setTimeout(function() {
self.poll();
if (!self.isMounted()) {
return;
}
self._timer = setInterval(self.poll(), 15000);
}, 1000);
},
poll: function() {
if (!this.isMounted()) {
return;
}
var self = this;
console.log('hello');
$.get(this.props.source, function(result) {
if (self.isMounted()) {
self.setState({
error: false,
error_message: '',
users: result
});
}
}).fail(function(response) {
self.setState({
error: true,
error_message: response.statusText
});
});
}
}
Note the console.log('hello'); in the poll function. I should see this every 15 seconds according to this logic.
Now lets look at a react component:
//= require ../../mixins/common/polling.js
//= require ../../mixins/common/state_handler.js
//= require ../../components/recent_signups/user_list.js
var RecentSignups = React.createClass({
mixins: [Polling, StateHandler],
getInitialState: function() {
return {
users: null,
error_message: '',
error: false
}
},
componentDidMount: function() {
this.startPolling();
},
componentWillUnmount: function() {
if (this._timer) {
clearInterval(this._timer);
this._timer = null;
}
},
shouldComponentUpdate: function(nextProps, nextState) {
if (this.state.users !== nextState.users ||
this.state.error !== nextState.error ||
this.state.error_message !== nextState.error_message) {
return true;
}
return false;
},
renderContents: function() {
if (this.state.users === null) {
return;
}
return (
<div>
<ul>
<UserList users={this.state.users} />
</ul>
</div>
);
},
render: function() {
return (
<div>
{this.loading()}
{this.errorMessage()}
{this.renderContents()}
</div>
)
}
});
RecentSignupsElement = document.getElementById("recent-signups");
if (RecentSignupsElement !== null) {
ReactDOM.render(
<RecentSignups source={ "http://" + location.hostname + "/api/v1/recent-signups/" } />,
RecentSignupsElement
);
}
Here we see in the componetDidMount function I am calling this.startPolling When the page loads, what I see after 1 second is:
hello
hello
A) its (poll fucntion) some how being called twice oO.
B) its (poll function) never being called again.
The reason I separated polling out is so that I can use it in other components on the same page and not duplicate code.
Very simply question(s):
Why and how do I fix this? I need it to poll ever 15 seconds and I should only see hello once when poll is called the first time.
On this line you call self.poll() and the result would be the timer:
self._timer = setInterval(self.poll(), 15000);
Instead pass the function:
self._timer = setInterval(self.poll, 15000);
As another option, in the spirit of "you're code's not working? just use someone else's instead!", react-async-poll is a handy component wrapper that you can use for polling.
This is my javascript class
var CommunicationInterface = inherit(Interface, {
__constructor: function() {
this.heartBeatDuration = 60 * 1000;
this.client = null;
this._isSetupped = false;
this.__base();
},
setup: function() {
// console.log('xmpp interface setup started');
var _this = this;
var deferred = Q.defer();
if (this._isSetupped) {
deferred.resolve();
return deferred.promise;
}
CommConfig.get('params')
.then(function(params) {
_this.client = new Client({
id: params.id + '#' + config('HOST'),
password: params.password,
host: config('HOST'),
port: config('PORT'),
reconnect: true
});
_this.client.on('online', _this.onOnline.bind(_this));
setInterval(function() {
_this.heartBeat.bind(_this)(params.id);
}, _this.heartBeatDuration);
_this._isSetupped = true;
deferred.resolve();
}, function(err){
console.log(err);
});
return deferred.promise;
},
heartBeat: function(Id) {
var stanza = this._makeMessage({
'to': id + '#' + config('HOST'),
'type': 'chat'
}, '{}');
console.log('foo');
this.client.send(stanza);
console.log('bar');
},
onOnline: function() {
console.log('online');
this.client.send('online');
this.emitter.emit('online');
},
});
and the test code is:
describe('CommunicationInterface', function() {
var commInterface;
var stubGetConfig, stubCommClient, stubCommClientConnect, spyCommClientSend;
var clock;
before(function () {
var deferred = Q.defer();
stubGetConfig = sinon.stub(CommConfig, 'get')
.withArgs('params')
.returns(deferred.promise);
deferred.resolve({
'id': 'test',
'password': '123456',
});
stubCommClientConnect = sinon.stub(CommunicationInterface.Client.prototype,
'connect');
clock = sinon.useFakeTimers();
});
beforeEach(function () {
commInterface = new CommunicationInterface();
stubCommClient = sinon.spy(commInterface.client);
});
afterEach(function () {
stubCommClientConnect.reset();
stubGetConfig.reset();
stubCommClient.reset();
clock.restore();
});
it('test 1', function(done) {
commInterface.setup()
.done(function () {
var spyCommClientSend = sinon.spy(commInterface.client, 'send');
commInterface.client.emit('online');
assert.isTrue(spyCommClientSend.calledOnce);
assert.isTrue(spyCommClientSend.calledWithExactly('online'));
done();
});
});
it('test 2', function(done) {
var spyHeartBeat = sinon.spy(commInterface.__proto__, 'heartBeat');
commInterface.setup().done(function() {
var spyCommClientSend = sinon.spy(commInterface.client, 'send');
clock.tick(commInterface.heartBeatDuration + 10);
assert.isTrue(spyHeartBeat.calledOnce);
assert.isTrue(spyCommClientSend.called);
spyHeartBeat.restore();
done();
});
});
});
The code in test 1 is working fine and spyCommClientSend is created properly, but the second assertion in test 2 fails and spyCommClientSend does not spy the actual object.
What could possibly be the reason here?
I am sure the send function is being called because the two console.log statements around it are printed.
At first glance I think the problem is in the fact that your spy is looking at commInterface.__proto__.heartBeat, which means that you are asserting that the heartBeat method on the CommunicationInterface prototype is called. This will not happen, because when you make sinon's clock tick, the heartBeat call is on the commInterface instance you created inside beforeEach.
This might be fixed by actually spying on heartBeat on the instance instead of the prototype, like so:
var spyHeartBeat = sinon.spy(commInterface, 'heartBeat');
Additionally, I would suggest that you cleanup commInterface inside your afterEach call by setting it to undefined or null -- just to ensure that you have a brand new, completely clean instance of CommunicationInterface with every test case.
Hope this helps!
how does one test a code inside a callback function using sinon.js framework for mocking?
JSFiddle: http://jsfiddle.net/ruslans/CE5e2/
var service = function () {
return {
getData: function (callback) {
return callback([1, 2, 3, 4, 5]);
}
}
};
var model = function (svc) {
return {
data: [],
init: function () {
var self = this;
svc.getData(function (serviceData) {
self.data = serviceData; // *** test this line ***
});
}
}
};
I use mocha tests with chai but am familiar with qUnit, so any of these tests would be accepted.
callsArgWith(0, dataMock) does the trick:
http://jsfiddle.net/ruslans/3UtdF/
var target,
serviceStub,
dataMock = [0];
module("model tests", {
setup: function () {
serviceStub = new service();
sinon.stub(serviceStub);
serviceStub.getData.callsArgWith(0, dataMock);
target = new model(serviceStub);
},
teardown: function () {
serviceStub.getData.restore();
}
});
test("data is populated after service.getData callback", function () {
target.init();
equal(target.data, dataMock);
});
I'm having some trouble with getting a simple async test working. The following piece of code doesn't throw any errors in the console, even though it should, because the data passed to the function does not equal 0:
define([
'intern!bdd',
'intern/chai!expect'
], function (bdd, expect) {
with (bdd) {
describe('Test', function () {
it('async test', function(){
var dfd = this.async(2000);
var wait = function(ms) {
setTimeout(function(){
dfd.resolve('test');
}, ms);
return dfd.promise;
};
wait(1500).then(dfd.callback(function (data) {
// data === 'test', so this should fail.
expect(data).to.equal(0);
}), dfd.reject.bind(dfd));
});
});
}
});
I'm pretty sure I messed up somewhere because I never worked with promises until now, but I can't figure out where. Any ideas would help a lot. Thanks!
You’re using the same Deferred object for two different asynchronous operations and resolving it (= successful test) the first time. You need to create your own separate Deferred object for the wait function:
define([
'intern!bdd',
'intern/chai!expect',
'intern/node_modules/dojo/Deferred'
], function (bdd, expect, Deferred) {
with (bdd) {
describe('Test', function () {
it('async test', function(){
var dfd = this.async(2000);
var wait = function(ms) {
var waitDfd = new Deferred();
setTimeout(function(){
waitDfd.resolve('test');
}, ms);
return waitDfd.promise;
};
wait(1500).then(dfd.callback(function (data) {
// data === 'test', so this should fail.
expect(data).to.equal(0);
}), dfd.reject.bind(dfd));
});
});
}
});
I have an object
var actions = {
'photos': function()
{
var self = this; // self = actions
$.get('./data.php?get=photos', function(data)
{
self.result = data;
});
},
'videos': function()
{
var self = this;
$.get('./data.php?get=videos', function(data)
{
self.result = data;
});
}
};
Each function creates one more item in actions called result
Then, instead of switch I use this (works good):
if (actions[action])
{
actions[action](); // call a function
console.log(actions);
console.log(actions.result);
}
action is a variable with value photos or videos.
console.log(actions) gives this:
Object
message: function ()
messages: function ()
profile: function ()
profile-edit: function ()
result: "<div>...</div>"
__proto__: Object
So I think there is resultitem in actions with the value "<div>...</div>".
But, console.log(actions.result) returns undefined.
Why?
I know all this code may be rewrited, but I would like to understand the reason of undefined.
Because we are dealing with asynchronous requests, we use "callbacks".
A callback is called when an asynchronous request is ready. Your request will get a response, and you send that response with the callback. The callback handles the response.
var actions = {
'photos': function(callback)
{
$.get('./data.php?get=photos', callback);
},
'videos': function(callback)
{
$.get('./data.php?get=videos', callback);
}
};
var action = 'photos';
actions[action](function(data) {
console.log(data);
});
Since you ensist on keeping the values, I would use this structure:
var actions = {
'photos': function()
{
$.get('./data.php?get=photos', function() {
this.__callback('photos', data);
});
},
'videos': function()
{
$.get('./data.php?get=videos', function() {
this.__callback('videos', data);
});
},
'__callback': function(action, data) {
this.results[action].push(data);
},
'results': {
'photos': [],
'videos': []
}
};
var action = 'photos';
actions[action]();
// use a timeout because we are dealing with async requests
setTimeout(function() {
console.log(actions.results); // shows all results
console.log(actions.results.photos); // shows all photos results
console.log(actions.results.videos); // shows all videos results
}, 3000);
gaaah what a horrible piece of code...