Jasmine - correctly mocking an object with a constructor

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.