11
votes

I'm trying to test an application but jest.spyOn is make me crazy.

I'm working on node v8.9.4 and jest v22.2.2

I have extracted this code:

// my-module.js
class MySingletonModule {
  constructor() {
    this.foo = 'bar';
  }
  myPromise() {
    return new Promise((resolve, reject) => resolve(this.foo));
  }
}

module.exports = new MySingletonModule();

// promise.test.js
const singleton = require('./my-module');
describe('Module test-suite', () => {
  let spy;

  beforeAll(async () => {
    spy = jest.fn();
    spy = jest.spyOn(singleton, 'myPromise');
  });

  beforeEach(() => spy.mockReset());

  test('Check bar', () => {
    return singleton.myPromise().then((bar) => {
      expect(bar).toEqual('bar');
    });
  });

  test('Check called times', () => {
    singleton.myPromise();
    expect(spy).toHaveBeenCalledTimes(1);
  });

  afterAll(() => {
    jest.restoreAllMocks();
  });
});

The Check bar test is failing because the myPromise method doesn't return a promise:

the error

If I comment the spy = jest.spyOn(singleton, 'myPromise'); the test works.. but obviously the other test doesn't works..

enter image description here

I would expect that all the tests work with spyOn, because reading the docs is wrote:

Note: By default, jest.spyOn also calls the spied method.

I'm missing something?

Thanks for the help

2
just as a comment, you know you can use async functions if you're using node 8, like test('Check bar', async () => { const bar = await singleton.myPromise() expect(bar).toEqual('bar'); }); - zavr
mh, thats a weird one, maybe the spy = jest.fn(); line above leads to that? Try removing it - Johannes Merz
I have wrote that only for switch between line 7 and 8 easily. I tried that but nothing has changed - Manuel Spigolon
My guess is this is a failure to call through to the function. I would expect you get the exact same problem if you return anything, not just a Promise. Is that true? (For example, try returning the object { then: f=>f(this.foo) }) It might be related to mockReset, possibly? - apsillers
No, even with a simple return 42; the function, after is spied, return undefined - Manuel Spigolon

2 Answers

2
votes

Here is working snippet without Jasmine:

describe('Module test-suite', () => {
  let spy;

  beforeAll(() => { // get rid of async
    spy = jest.fn();
    spy = jest.spyOn(singleton, 'myPromise');
  });

  afterEach(() => spy.mockRestore()); // main difference is here

  test('Check bar', () => {
    return singleton.myPromise().then((bar) => {
      expect(bar).toEqual('bar');
    });
  });

  test('Check called times', () => {
    singleton.myPromise();
    expect(spy).toHaveBeenCalledTimes(1);
  });

  afterAll(() => {
    jest.restoreAllMocks();
  });
});

I think it's better to use mockRestore instead of mockReset, because as I understand from jest docs mockReset hardly clears all information about spy and spying object. And mockRestore just clears some temp data such as number of called times.

Why is using afterEach instead of beforeEach important - I have no clue :(

Also I removed async because tests failed with it.

2
votes

I can't see any mention of this in the documentation but it seems like jest allows you to use a lot of Jasmine's functionality / syntax, including .and.callThrough:

beforeEach(() => {
  spyOn(singleton, 'myPromise').and.callThrough();
};

test('Check bar', async () => {
  const bar = await singleton.myPromise();
  expect(bar).toEqual('bar');
});

test('Check called times', () => {
  singleton.myPromise();
  expect(singleton.myPromise.calls.count()).toBe(1);
});

Here are the jasmine spy docs: https://jasmine.github.io/2.0/introduction.html#section-Spies

If you want to stick to the documented jest.spyOn API then you can instead set the spy inside of the test that uses it, so that it doesn't affect the other:

test('Check bar', async () => {
  const bar = await singleton.myPromise();
  expect(bar).toEqual('bar');
});

test('Check called times', () => {
  const spy = jest.spyOn(singleton, 'myPromise');
  singleton.myPromise();
  expect(spy).toHaveBeenCalledTimes(1);
});