9

I'm trying to mock a function exported from a typescript file in a Jasmine test. I expect the following to mock the imported foo and return the value 1 in the spec for bar.

The mock appears to be uncalled, so I'm clearly missing something. How can I fix this example?

demo.ts:

export function foo(input: any): any {
  return 2;
}

export function bar(input: any): any {
  return foo(input) + 2;
}

demo.ts.spec:

import * as demo from './demo';

describe('foo:', () => {
  it('returns 2', () => {
    const actual = demo.foo(1);
    expect(actual).toEqual(2);
  });
});

describe('bar:', () => {
  // let fooSpy;
  beforeEach(() => {
    spyOn(demo, 'foo' as any).and.returnValue(1); // 'as any' prevents compiler warning
  });

  it('verifies that foo was called', () => {
    const actual = demo.bar(1);
    expect(actual).toEqual(3); // mocked 1 + actual 2
    expect(demo.foo).toHaveBeenCalled();
  });
});

Failures:

  • Expected 4 to equal 3.
  • Expected spy foo to have been called.
Sevenless
  • 2,805
  • 4
  • 28
  • 47

2 Answers2

5

Jeffery's answer helped get me on the right track.

To attach the spy is attached to the right reference for foo the product code needs to have a small change. foo() should be called as this.foo()

The below pattern works for testing (and is a lot cleaner than the convoluted work around I was using previously).

demo.ts:

export function foo(input: any): any {
  return 2;
}

export function bar(input: any): any {
  return this.foo(input) + 2;
}

demo.ts.spec:

import * as demo from './demo';

describe('foo:', () => {
  it('returns 2', () => {
    const actual = demo.foo(1);
    expect(actual).toEqual(2);
  });
});

describe('bar:', () => {
  // let fooSpy;
  beforeEach(() => {
    spyOn(demo, 'foo' as any).and.returnValue(1);
  });

  it('verifies that foo was called', () => {
    const actual = demo.bar(1);
    expect(actual).toEqual(3);
    expect(demo.foo).toHaveBeenCalled();
  });
});
Sevenless
  • 2,805
  • 4
  • 28
  • 47
  • 1
    This is true only for classes. a raw function does not have a this. Well kinda. Typescript complains about it. – Samuel Thompson Apr 09 '18 at 21:24
  • Agree. This is a typical use-case where you'd need an instantiable object that uses a [strategy](https://en.wikipedia.org/wiki/Strategy_pattern) for each _unit_ you want to be able to mock independently. – JJWesterkamp Apr 09 '18 at 21:42
  • This saved my life, thank you! BTW, how did you get rid of the "'this' implicitly has type 'any' because it does not have a type annotation."? NVM, I figured it out, it was the noImplicitThis option, not the noImplicitAny – Hansen W Aug 30 '19 at 05:55
4

From this issue on Github: How are you expecting to use the spied on function in your actual implementation?

Your bar implementation calls the actual implementation of foo, because it has a direct reference to it. When importing in another module, a new object, with new references, is created:

// This creates a new object { foo: ..., bar: ... }
import * as demo from './demo';

These references exist only in the module of the import. When you call spyOn(demo, 'foo') it's that reference that is being used. You might want to try this in your spec, chances are the test passes:

demo.foo();
expect(demo.foo).toHaveBeenCalled();

Expecting the real implementation of bar to call a mocked foo is not really possible. Instead try to treat bar as if it had its own implementation of foo.

JJWesterkamp
  • 7,559
  • 1
  • 22
  • 28