Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

jasmine spies on inherited methods (with typescript) not working as expected with toHaveBeenCalled()

I'm currently having an issue when spying on inherited methods for calls in typescript classes, where the toHaveBeenCalled() method is returning false, even though the method being spied upon is called. Look at the following scenario...

I have two classes, written in TypeScript

class Parent() {     buyFood() {         // buy food     } }  class Husband extends Parent {     makeDinner() {         super.buyFood();         // make dinner;     } } 

In my tests for the class Husband, i'm only concerned of testing the logic for making dinner, since the logic of the buy food of the super class is tested in its own test suite.

Hence, my tests looks like something of the following sort.

let husband:Husband = new Husband();  it('Should make a good dinner', () => {     spyOn(husband, 'buyFood');     husband.makeDinner();      expect(husband.buyFood).toHaveBeenCalled(); } 

Even though buyFood() is being called, the assertion is failing with an error saying that husband.buyFood() which is the method inherited from the Parent class has never been called.

How should I go about this issue, without having to assert the value changes by the buyFood() method call?

like image 631
JeanPaul A. Avatar asked Jan 26 '17 08:01

JeanPaul A.


2 Answers

You have to understand the mechanics behind Typescript and the spying.

First on Typescript ...

I'm ignoring the extra parens in class Parent().

Typescript uses prototypal inheritance behind the curtain. Thus a prototype will copy the referenced properties from the "base class" to the new class. This is what the for loop does in the __extends() function.

This is the ES5 code your Typescript is translated to:

var __extends = (this && this.__extends) || function (d, b) {     for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];     function __() { this.constructor = d; }     d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }; var Parent = (function () {     function Parent() {     }     Parent.prototype.buyFood = function () {         // buy food     };     return Parent; }()); var Husband = (function (_super) {     __extends(Husband, _super);     function Husband() {         return _super.apply(this, arguments) || this;     }     Husband.prototype.makeDinner = function () {         _super.prototype.buyFood.call(this);         // make dinner;     };     return Husband; }(Parent)); 

You can translate typescript using this Typescript playground.

Your super expression calls the buyFood() method of the parent class and not the method of the "inherited" Husband.

See the line

_super.prototype.buyFood.call(this); 

and follow the _super reference.

Now Jasmine Spies ...

A spy will replace the named function of the passed object by a spy function that will act as a proxy. That proxy can now track calls and, depending on the programmed behavior, control whether to call the original function, a fake, return a value or do nothing (default).

A very simplified spyOn() could look like this:

function spyOn(obj, fn) {     var origFn = obj[fn],         spy = function() {             spy.calls.push(arguments);         };      spy.calls = [];      obj[fn] = spy; } 

The actual spy method is much more complex though.

Your line

spyOn(husband, 'buyFood'); 

will actually replace the method in the instance of Husband by a spy. But, since the code calls the reference of the base class (the parent prototype) it's not the same function that you've just replaced.

Solution

You should either call the this referenced method

class Husband extends Parent {     makeDinner() {         // call byFood() via this         this.buyFood();     } } 

... or spy on the parent prototype (super):

it('Should make a good dinner', () => {     spyOn(Parent.prototype, 'buyFood');     husband.makeDinner();      expect(Parent.prototype.buyFood).toHaveBeenCalled(); } 
like image 173
try-catch-finally Avatar answered Sep 19 '22 06:09

try-catch-finally


When using ES6, the Parent.prototype will not work. Use the Object.getPrototypeOf instead.

This is what worked for me:

it('Should make a good dinner', () => {     spyOn(Object.getPrototypeOf(Object.getPrototypeOf(husband)), 'buyFood');     husband.makeDinner();      expect(Parent.prototype.buyFood).toHaveBeenCalled(); } 
like image 27
razi136 Avatar answered Sep 19 '22 06:09

razi136