66

I want to test whether the following method is called with in my Javascript object constructor. From what I have seen in the Jasmine documentation, I can spy on a constructor method and I can spy on methods after an object has been instantiated, but I can't seem to be able to spy on a method before the object is constructed.

The object:

Klass = function() {
    this.called_method();
};

Klass.prototype.called_method = function() {
  //method to be called in the constructor.
}

I want to do something like this in the spec:

it('should spy on a method call within the constructor', function() {
    spyOn(window, 'Klass');
    var obj = new Klass();
    expect(window.Klass.called_method).toHaveBeenCalled();
});
stillmotion
  • 4,608
  • 4
  • 30
  • 36

2 Answers2

116

Spy directly on the prototype method:

describe("The Klass constructor", function() {
  it("should call its prototype's called_method", function() {
      spyOn(Klass.prototype, 'called_method');  //.andCallThrough();
      var k = new Klass();
      expect(Klass.prototype.called_method).toHaveBeenCalled();
  });
});
Dave Newton
  • 158,873
  • 26
  • 254
  • 302
  • 3
    thank you for this. best description i've seen all day on the subject – Subimage Jun 03 '12 at 02:00
  • Had a hunch there was a legitimate solution to this out there. Thank you. – BradGreens Oct 10 '13 at 22:17
  • There are two problems with this approach. Firstly, it causes a memory leak - the called_method method all future instances of Klass will be spied upon and as more calls are made, the memory consumption of the spy will increase. Secondly and more concerningly, this also potentially causes multiple tests that interrogate Klass to interact with each other, as the spy will have already been called. You should make sure the spy is deleted or Klass.prototype.called_method reset to the original method at the end of the test case. – alecmce Nov 19 '13 at 01:49
  • 1
    @alecmce Please answer the question then. I asked the Jasmine guys is this was a valid approach and was told it was with a caveat, if it isn't, seems like answering it with a safer solution would be in order, no? – Dave Newton Nov 19 '13 at 01:57
  • Sorry, it is a valid answer, and I don't mean to imply it isn't. Only, I think it's worth cautioning people about potential problems with this approach. I'll expand in an answer. – alecmce Nov 19 '13 at 03:30
  • 2
    Jasmine 1.3.1+ fully restore functions after the spec has completed, so side effects should no longer be an issue, and resetting the method is no longer necessary. Yay! For reference: https://github.com/jasmine/jasmine/issues/236 – Cole Reed Feb 16 '15 at 01:41
12

Broadly, I agree with Dave Newton's answer above. However, there are some edge-cases to this approach that you should consider.

Take a variation to Dave's solution, with another test-case:

// production code
var Klass = function() {
  this.call_count = 0;
  this.called_method();
};
Klass.prototype.called_method = function() {
  ++this.call_count;
};

// test code
describe("The Klass constructor", function() {
  it("should call its prototype's called_method", function() {
    spyOn(Klass.prototype, 'called_method');
    var k = new Klass();
    expect(k.called_method).toHaveBeenCalled();
  });
  it('some other test', function() {
    var k = new Klass();
    expect(k.call_count).toEqual(1);
  });
});

The second test will fail because the spy setup in the first test persists across the test boundaries into the second method; called_method doesn't increment call_count, so this.call_count does not equal 1. It's also possible to come up with scenarios with false positives - tests that pass, that shouldn't.

On top of this, because the spy remains, the more Klass instances that are created, the bigger the memory heap the spy will consume, because the spy will record each call to called_method. This probably isn't a problem in most circumstances, but you should be aware of it, just in case.

A simple solution to this problem would be to make sure that the spy is removed after it has been used. It can look a bit ugly, but something like this works:

// test code
describe("The Klass constructor", function() {
  it("should call its prototype's called_method", function() {
    var spy = jasmine.createSpy('called_method');
    var method = Klass.prototype.called_method;
    Klass.prototype.called_method = spy;
    var k = new Klass();
    expect(spy).toHaveBeenCalled();
    Klass.prototype.called_method = method;
  });

[NOTE - a little opinion to finish] A better solution would be to change the way you write production code to make the code easier to test. As a rule, spying on prototypes is probably a code-smell to be avoided. Instead of instantiating dependencies in the constructor, inject them. Instead of doing initialization in the constructor, defer to an appropriate init method.

Will Shaver
  • 12,471
  • 5
  • 49
  • 64
alecmce
  • 1,456
  • 10
  • 23
  • 1
    +1, thanks for the additional information. I think the bottom "NOTE" section pretty much sums everything up; it's almost certain that it should be fixed elsewhere rather than in the test. – Dave Newton Dec 03 '13 at 20:22
  • Yeah, I think they closed this gotcha a while ago. Still worth avoiding spying on prototypes as a matter of principle. – alecmce Nov 14 '17 at 13:19