Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Jest spyOn() calls the actual function instead of the mocked #6972

Closed
NERDYLIZARD opened this issue Sep 13, 2018 · 27 comments
Closed

Jest spyOn() calls the actual function instead of the mocked #6972

NERDYLIZARD opened this issue Sep 13, 2018 · 27 comments

Comments

@NERDYLIZARD
Copy link

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

@rickhanlonii
Copy link
Member

rickhanlonii commented Nov 27, 2018

@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());

@JonathanHolvey
Copy link

JonathanHolvey commented Apr 12, 2019

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())
})

@jmls
Copy link

jmls commented Apr 22, 2019

@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''        });
});

@jmls
Copy link

jmls commented Apr 22, 2019

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

@codeepic
Copy link

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?

@kcabhish
Copy link

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

@mikeyaworski
Copy link

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

@mmattosr
Copy link

Same issue here!

@tranvansang
Copy link
Contributor

I have same issue when try mocking Date.now

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

@Kate-te
Copy link

Kate-te commented Jul 4, 2019

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
    });
});

@Kate-te
Copy link

Kate-te commented Jul 4, 2019

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

@lucasfcosta
Copy link
Contributor

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.

@tranvansang
Copy link
Contributor

@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}]
    ]
  }

@lucasfcosta
Copy link
Contributor

lucasfcosta commented Jul 7, 2019

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 💖

@tranvansang
Copy link
Contributor

@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

@lucasfcosta
Copy link
Contributor

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?

@tranvansang
Copy link
Contributor

tranvansang commented Jul 12, 2019

@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

@lucasfcosta
Copy link
Contributor

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.

@tfmertz
Copy link

tfmertz commented Jul 12, 2019

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.

@rockneverdies55
Copy link

Did anyone figure out why this is happening?

@SimenB
Copy link
Member

SimenB commented Aug 23, 2019

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.

#6972 (comment): same issue
#6972 (comment): same issue
#6972 (comment): 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

@JoshuaEDeFord
Copy link

JoshuaEDeFord commented Aug 30, 2019

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 ().

@JeannelMYT
Copy link

JeannelMYT commented Jul 27, 2020

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));

@kevinkaishao
Copy link

kevinkaishao commented Oct 7, 2020

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.

@Anutrix
Copy link

Anutrix commented Feb 1, 2021

Still having this issue. Not sure why this is closed.

@LukasBombach
Copy link

I found a solution, at least for my use-case. I was trying to mock useEffect on React:

import React from 'react';
import { render } from '@testing-library/react';
import { MyComponentThatUsesUseEffect } from './MyComponentThatUsesUseEffect'

test('My test case', () => {
  const useEffectSpy = jest.spyOn(React, 'useEffect').mockImplementation(() => {});
  render(<MyComponentThatUsesUseEffect />);
  expect(useEffectSpy).toHaveBeenCalled();
});

Didn't work. But then I was changing

- import React from 'react';
+ import * as React from 'react';

So I have this

⬇️

import * as React from 'react';
import { render } from '@testing-library/react';
import { MyComponentThatUsesUseEffect } from './MyComponentThatUsesUseEffect'

test('My test case', () => {
  const useEffectSpy = jest.spyOn(React, 'useEffect').mockImplementation(() => {});
  render(<MyComponentThatUsesUseEffect />);
  expect(useEffectSpy).toHaveBeenCalled();
});

Now it does work 👍🏼

Maybe this will work for your thing to and or give you a clue of what's happening

@github-actions
Copy link

This issue has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.
Please note this issue tracker is not a help forum. We recommend using StackOverflow or our discord channel for questions.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators May 10, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests