I'm trying to mock the native WebSocket in a jasmine test for Angular, I can spy on the constructor and send
function, but I can't figure out how to spoof a call of onmessage
.
WebSocket is extracted to an Angular constant: webSocket
.
My test looks like this:
describe('Data Service', function () {
var dataService,
$interval,
ws;
beforeEach(function () {
module('core', function ($provide) {
ws = jasmine.createSpy('constructor');
ws.receiveMessage = function (message) {
this.onmessage(message);
};
$provide.constant('webSocket', ws);
});
inject(function (_dataService_, _$interval_) {
dataService = _dataService_;
$interval = _$interval_;
});
});
it("should call subscribers when a message is received", function () {
var callback = jasmine.createSpy('onMessage callback');
function message(type) {
return {
data: {
type: type,
data: 'data'
}
};
}
// Subscribe to messages via the exposed function.
// Subscribe to one of them twice to test that all subscribers are called and not just the first one.
dataService.onMessage(21, callback);
dataService.onMessage(21, callback);
dataService.onMessage(22, callback);
dataService.onMessage(23, callback);
// Pick 3 numbers that are valid data types to test.
ws.receiveMessage(message(21));
ws.receiveMessage(message(22));
ws.receiveMessage(message(23));
expect(callback.calls.count()).toBe(4);
expect(callback.calls.allArgs()).toBe([message(21).data, message(21).data, message(22).data, message(23).data]);
});
});
My code looks like this:
angular.module('core', []).constant('webSocket', WebSocket);
angular.module('core').factory('dataService', function ($interval, webSocket) {
function openSocket() {
sock = new webSocket('ws://localhost:9988');
sock.onmessage = function (message) {
var json = JSON.parse(message.data);
onMessageSubscribers[json.type].forEach(function (sub) {
sub(json);
});
};
}
function onMessage(type, func) {
onMessageSubscribers[type].push(func);
}
openSocket();
return {
onMessage: onMessage
};
});
onMessageSubscribers
is a defined array with the correct types in it (int keys), but it's not relevant to the problem.
I get the following error:
TypeError: 'undefined' is not a function (evaluating 'this.onmessage(message)')
onmessage
appears to be defined by the angular code that runs before the test, but I'm wondering if this is something to do with how a constructed object differs from a regular object in JS, I don't really have any experience with them.
I've tried a few different ways to do this, like calling ws.prototype.onmessage
or just ws.onmessage
.
If I place a console.log(sock.onmessage);
in dataService
at the appropriate place, and another log before I invoke onmessage
in the tests:
function (message) { ... }
undefined
How can I force an invoke of onmessage or any other WebSocket event?
When you call var sock = new webSocket();
new instance is being created from the webSocket
constructor. Because of your design, this sock
variable (webSocket
instance) is not publicly exposed, therefore is not available in a test suite. Then, you define a property onmessage
on this sock
instance: sock.onmessage = function () { ... }
.
What you want to do is to manually trigger onmessage
from the test suite, but onmessage
is attached to sock
instance, and because you don't actually have access to sock
instance from a test suite, therefore you don't have access to onmessage
either.
You can't even access it from the prototype chain, because you don't have actual sock
instance at hand.
What I've came up with is really tricky.
JavaScript has a feature - you can explicitly return object from a constructor (info here) and it will be assigned to a variable instead of a new instance object. For example:
function SomeClass() { return { foo: 'bar' }; }
var a = new SomeClass();
a; // { foo : 'bar' }; }
Here is how we can use it to access onmessage
:
var ws,
injectedWs;
beforeEach(function () {
module('core', function ($provide) {
// it will be an explicitly returned object
injectedWs = {};
// and it is a constructor - replacer for native WebSocket
ws = function () {
// return the object explicitly
// it will be set to a 'sock' variable
return injectedWs;
};
$provide.constant('webSocket', ws);
});
As a result, when in DataService
you do sock.onmessage = function () {};
, you actually assign it to that explicilty returned injectedWs
, and it will be available in our test suite, because of the JavaScript another feature - objects are passed by reference.
Finally, you now are able to call onmessage
in a test whenever you need using injectedWs.onmessage(message)
.
I've moved all the code here: http://plnkr.co/edit/UIQtLJTyI6sBmAwBpJS7.
Note: There were some issues with suite expectations and JSON parsings, probably because you were not able to reach this code yet, I've fixed them so the tests could run.