Jest: Jest spyOn() calls the actual function instead of the mocked

Created on 13 Sep 2018  路  24Comments  路  Source: facebook/jest

I'm testing apiMiddleware that calls its helper function callApi. To prevent the call to actual callApi which will issue the api call, I mocked the function. However, it still gets called.

apiMiddleware.js

import axios from 'axios';

export const CALL_API = 'Call API';

export const callApi = (...arg) => {
  return axios(...arg)
    .then( /*handle success*/ )
    .catch( /*handle error*/ );
};

export default store => next => action => {
  // determine whether to execute this middleware
  const callAPI = action[CALL_API];
  if (typeof callAPI === 'undefined') {
    return next(action)
  }

  return callAPI(...callAPI)
    .then( /*handle success*/ )
    .catch( /*handle error*/ );
}

apiMiddleware.spec.js

import * as apiMiddleware from './apiMiddleware';

const { CALL_API, default: middleware, callApi } = apiMiddleware;

describe('Api Middleware', () => {

  const store = {getState: jest.fn()};
  const next = jest.fn();
  let action;

  beforeEach(() => {
    // clear the result of the previous calls
    next.mockClear();
    // action that trigger apiMiddleware
    action = {
      [CALL_API]: {
        // list of properties that change from test to test 
      }
    };
  });

  it('calls mocked version of `callApi', () => {
    const callApi = jest.spyOn(apiMiddleware, 'callApi').mockReturnValue(Promise.resolve());

    // error point: middleware() calls the actual `callApi()` 
    middleware(store)(next)(action);

    // assertion
  });
});

Please ignore the action's properties and argument of callApi function. I don't think they are the concern of the point I'm trying to make.

Tell me if you need further elaboration.

stackoverflow

Most helpful comment

@NERDYLIZARD sorry for the delay here!

By default jest.spyOn() does not override the implementation (this is the opposite of jasmine.spyOn). If you don't want it to call through you have to mock the implementation:

const callApi = jest.spyOn(apiMiddleware, 'callApi').mockImplementation(() => Promise.resolve());

All 24 comments

@NERDYLIZARD sorry for the delay here!

By default jest.spyOn() does not override the implementation (this is the opposite of jasmine.spyOn). If you don't want it to call through you have to mock the implementation:

const callApi = jest.spyOn(apiMiddleware, 'callApi').mockImplementation(() => Promise.resolve());

I seem to be having this problem as well, but the solution that @rickhanlonii proposed isn't working for me. I'm following the documentation for jest.spyOn(), but the mocked function is still being called when running the tests.

processing.js

const saveVisit = (visit) => {
  return database.put(visit)
}

const processVisit = (visit) => {
  if (visit.status.includes('PROCESSED') {
    saveVisit(visit)
    return promise.resolve()
  }

  saveVisit({ ...visit, status: ['PROCESSED'] })
  return visit.status
}

processing.test.js

const subject = require('../processing')

test('processVisit for processed visit returns null', () => {
  const visit = { status: ['PROCESSED'] }

  jest.spyOn(subject, 'saveVisit').mockImplementation(() => Promise.resolve())
  return subject.processVisit(visit).then(result => expect(result).toBeNull())
})

@JonathanHolvey : did you solve this problem ? I seem to have hit it - but the weird thing is that an "it()" above the failing spy does work

// this test spyon works .. 
it('should list users', async () => {
        jest.spyOn(service, 'listUsers').mockImplementation(() =>
            Promise.resolve(["test"])
        );
        const result = await controller.listUsers();
        expect(result).toEqual(['test']);
    });
// this one doesn't ... 
it('should create a user', async () => 
        jest.spyOn(service, 'createUser').mockImplementation(() => Promise.resolve(null);
       );
        return controller.createUser({            username: 'test''        });
});

ah, just forget what I said. Brain fart - my controller was calling the wrong service ...

Why is this issue closed, since it's not resolved?
I encountered this problem when trying to prevent Jest from calling the spied method.
I tried jest.fn() and .mockImplementation(() => {}) and the original method is still called from the test.

I opened a question on StackOverflow:

https://stackoverflow.com/questions/55852730/jest-when-using-spyon-function-ensure-the-spied-one-is-not-called

Any solutions?

I am running into the same issue. I even tried the mockImplementation but still it hits the original function.

I'm having the same issue with something like this:

original.js

export const original = () => {
  console.log('original function');
}

original.test.js

import * as stuff from './original.js';

...

const mySpy = jest.spyOn(stuff, 'original').mockImplementation(() => {
  console.log('mock function');
});

The output is

mock function
original function

Same issue here!

I have same issue when try mocking Date.now

jest.mock(Date, 'now').mockImplementation(() => 1); expect(Date.now()).toBe(1) does not pass

i solved same problem by exporting default object with all methods, so @NERDYLIZARD's code would look like that:
apiMiddleware.js

import axios from 'axios';

export const CALL_API = 'Call API';

export default {
    callApi(...arg) {
        return axios(...arg)
            .then( /*handle success*/ )
            .catch( /*handle error*/ );
    },
    middleware: store => next => action => {
        // determine whether to execute this middleware
        const callAPI = action[CALL_API];
        if (typeof callAPI === 'undefined') {
            return next(action)
        }

        return callAPI(...callAPI)
            .then( /*handle success*/ )
            .catch( /*handle error*/ );
    }
}

apiMiddleware.spec.js

import apiMiddleware, { CALL_API } from './apiMiddleware';

describe('Api Middleware', () => {

    const store = {getState: jest.fn()};
    const next = jest.fn();
    let action;

    beforeEach(() => {
        // clear the result of the previous calls
        next.mockClear();
        // action that trigger apiMiddleware
        action = {
            [CALL_API]: {
                // list of properties that change from test to test 
            }
        };
    });

    it('calls mocked version of `callApi', () => {
        const callApi = jest.spyOn(apiMiddleware, 'callApi').mockReturnValue(Promise.resolve());

        // error point: middleware() calls the actual `callApi()` 
        apiMiddleware.middleware(store)(next)(action);

        // assertion
    });
});

@tranvansang try Date.now = jest.fn(() => 1)

Hello everyone 馃槉

As I was taking a look into this I first tried to add a very simple test to check whether this specific behaviour was present in the current version of master. The test-case below is based on one of the comments in this issue.

it('does not call the mocked function', () => {
  let originalCallCount = 0;
  let fakeCallCount = 0;
  const obj = {fn: () => originalCallCount++};

  moduleMocker.spyOn(obj, 'fn').mockImplementation(() => fakeCallCount++);

  obj.fn();
  expect(originalCallCount).toBe(0);
  expect(fakeCallCount).toBe(1);

  obj.fn();
  expect(originalCallCount).toBe(0);
  expect(fakeCallCount).toBe(2);
});

I also tried the test-case suggested by @tranvansang and I didn't find problems:

it('works for dates', () => {
  moduleMocker.spyOn(Date, 'now').mockImplementation(() => 1);
  expect(Date.now()).toBe(1);
});

This test passes just fine, demonstrating that the original function is never actually called. This means the behaviour seems correct on jest's side.

Then I went on to check for edge-cases but none caused the tests to call the original function. I even checked whether it could be because now could be a non-writable property, but that's not the case and has never been AFAIK. I imagined that could be the case for when using esmodules, but if it fails loudly in the case of Date.now the behaviour would be the same even if that was true.

> Object.getOwnPropertyDescriptor(Date, "now")
{ value: [Function: now],
  writable: true,
  enumerable: false,
  configurable: true }

However, tests would indeed fail when the function property we're trying to mock is not writable, which means we cannot assign to it using the = operator. However, tests would fail loudly instead of calling the original function as is the behaviour described above.

For this, I used a variation of the first test.

it('works for non-writable properties', () => {
  let originalCallCount = 0;
  let fakeCallCount = 0;
  const obj = {};
  Object.defineProperty(obj, 'fn', {
    value: () => originalCallCount++,
    writable: false,
    configurable: true,
  });

  moduleMocker.spyOn(obj, 'fn').mockImplementation(() => fakeCallCount++);

  obj.fn();
  expect(originalCallCount).toBe(0);
  expect(fakeCallCount).toBe(1);

  obj.fn();
  expect(originalCallCount).toBe(0);
  expect(fakeCallCount).toBe(2);
});

The test above will fail with the following error:

TypeError: Cannot assign to read only property 'fn' of object '#<Object>'

In the case above it doesn't need to fail. It could simply use Object.defineProperty instead of the = operator, which would work since we can change the property descriptor and pass a different value due to this property being configurable but we cannot change the value using = due it not being writable.

I can't think of any other ways of reproducing this.

If any of you could provide a minimum reproducible snipped I wouldn't mind looking into it and checking why it happens and if it's a problem in jest's side or not.

@KateBog that did not work, though.

@lucasfcosta have you tried with some babel configuration?

I remember while debug, some babel plugins transpile all Date.now to a new variable named dateNow.

The only way I can make it work is here

My babel config you can try if want to reproduce

{
    presets: [
      ['@babel/preset-env', {modules: 'commonjs', useBuiltIns: 'entry', corejs}],
      '@babel/preset-react',
      ['@babel/preset-typescript', {isTSX: true, allExtensions: true}]
    ],
    plugins: [
      '@babel/plugin-proposal-object-rest-spread',
  // 'react-hot-loader/babel',
  // Stage 0
  '@babel/plugin-proposal-function-bind',

  // Stage 1
  '@babel/plugin-proposal-export-default-from',
  '@babel/plugin-proposal-logical-assignment-operators',
  ['@babel/plugin-proposal-optional-chaining', {'loose': false}],
  ['@babel/plugin-proposal-pipeline-operator', {'proposal': 'minimal'}],
  ['@babel/plugin-proposal-nullish-coalescing-operator', {'loose': false}],
  '@babel/plugin-proposal-do-expressions',

  // Stage 2
  ['@babel/plugin-proposal-decorators', {'legacy': true}],
  '@babel/plugin-proposal-function-sent',
  '@babel/plugin-proposal-export-namespace-from',
  '@babel/plugin-proposal-numeric-separator',
  '@babel/plugin-proposal-throw-expressions',

  // Stage 3
  '@babel/plugin-syntax-dynamic-import',
  '@babel/plugin-syntax-import-meta',
  ['@babel/plugin-proposal-class-properties', {'loose': false}],
  '@babel/plugin-proposal-json-strings',
  'react-loadable/babel',,
      ['@babel/plugin-transform-runtime', {corejs}]
    ]
  }

Hi, @tranvansang thanks for your clarification 馃槉

Do you think it would be possible for you to provide a repo with a minimum reproducible?

It's a bit difficult to track down the problem by trying to put multiple separate pieces together especially since I don't have the same context as you when it comes to all the post-processing applied to the code or how it gets built before it runs or even what code does jest actually run against.

As per my post above, I don't think there's anything wrong on Jest's side but instead, I suspect there's something weird happening elsewhere (perhaps on any of the transformations that happen to your code). If that's the case maybe we could suggest adding something specific in jest to manage that edge-case, but first, we need to have a minimum reproducible we can work from.

Thanks for understanding 馃挅

@lucasfcosta

Here is the minimal repo

https://github.com/tranvansang/flip-promise/tree/now

It is definitely because of the @babel/plugin-transform-runtime as I comment here

Hi @tranvansang,

I just cloned the repo you have mentioned and there are no tests using mocks. I tried to add one myself (the one for Date.now that you had mentioned) but it still passes.

Are you sure you linked the correct repo? If you did, how can I reproduce this issue there?

@lucasfcosta that is the repo for my public package.

I made a branch named now for the bug reproduction. Please use that branch

Did you try this test?

https://github.com/tranvansang/flip-promise/blob/now/index.test.ts#L3

Ah, it makes sense now, I had tried master before. I'll give it a go in the weekend and I'll let you know how that goes. But in advance: this is probably something that's not solvable in Jest's side even though it could be enlightening to see why it happens or maybe find-out what we can do to fix it.

For me, this was an error because of how modules were being imported. I was mocking a function inside the same file as the function I was calling.

More details about it here: https://stackoverflow.com/questions/45111198/how-to-mock-functions-in-the-same-module-using-jest

Importing the module into itself and using it as a reference seemed to solve it, although kinda janky:

import * as thisModule from './module';

export function bar () {
    return 'bar';
}

export function foo () {
    return `I am foo. bar is ${thisModule.bar()}`;
}

Example from stackoverflow

Not the greatest, but works. Otherwise, take the function out into a different file.

Did anyone figure out why this is happening?

None of the examples proved in this issue are correct usage of spyOn.

From the OP, middleware is an object that just exists within the test file - replacing a function on that object won't have any effect outside of the lexical scope that object is inside of.

https://github.com/facebook/jest/issues/6972#issuecomment-482478861: same issue
https://github.com/facebook/jest/issues/6972#issuecomment-501386592: same issue
https://github.com/facebook/jest/issues/6972#issuecomment-505286524: uses jest.mock instead of jest.spyOn

A PR improving the docs here would be greatly appreciated as it seems we're not clear enough on how it works. There's no magic here - we literally replace a function of the name on the object you pass, and call through to it.


If anyone can put together a small repo showing the error (or a code sandbox) showing how spyOn doesn't work, that'd be great. Small snippets and links to SO are all well and good, but it requires more effort for anyone wanting to investigate this.

https://www.snoyman.com/blog/2017/10/effective-ways-help-from-maintainers

My solution involved making sure to define the mockImplementation as async correctly. I'm guessing that, since the mocks in these examples return promises they are mocking async functions. So the anonymous mock should also be defined as async: async () not just ().

I managed to get past this with reference to this blog post

like:

import {ExampleService} from '../example.service'
...
..

const exampleMockResponse = "....";

jest.spyOn(ExampleService.prototype, 'function').mockImplementation(() => Promise.resolve(exampleMockResponse));

In case anyone is still plagued by this issue, this short article does a great job of explaining the root cause (it is due to babel compilation). Arguably it's not pretty, but adding the additional layer of indirection worked for me.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

Antho2407 picture Antho2407  路  3Comments

samzhang111 picture samzhang111  路  3Comments

kentor picture kentor  路  3Comments

hramos picture hramos  路  3Comments

Secretmapper picture Secretmapper  路  3Comments