How to change mock implementation on a per single test basis?
Asked Answered
C

8

244

I'd like to change the implementation of a mocked dependency on a per single test basis by extending the default mock's behaviour and reverting it back to the original implementation when the next test executes.

More briefly, this is what I'm trying to achieve:

  1. Mock dependency
  2. Change/extend mock implementation in a single test
  3. Revert back to original mock when next test executes

I'm currently using Jest v21. Here is what a typical test would look like:

// __mocks__/myModule.js

const myMockedModule = jest.genMockFromModule('../myModule');

myMockedModule.a = jest.fn(() => true);
myMockedModule.b = jest.fn(() => true);

export default myMockedModule;
// __tests__/myTest.js

import myMockedModule from '../myModule';

// Mock myModule
jest.mock('../myModule');

beforeEach(() => {
  jest.clearAllMocks();
});

describe('MyTest', () => {
  it('should test with default mock', () => {
    myMockedModule.a(); // === true
    myMockedModule.b(); // === true
  });

  it('should override myMockedModule.b mock result (and leave the other methods untouched)', () => {
    // Extend change mock
    myMockedModule.a(); // === true
    myMockedModule.b(); // === 'overridden'
    // Restore mock to original implementation with no side effects
  });

  it('should revert back to default myMockedModule mock', () => {
    myMockedModule.a(); // === true
    myMockedModule.b(); // === true
  });
});

Here is what I've tried so far:

  1. mockFn.mockImplementationOnce(fn)

    it('should override myModule.b mock result (and leave the other methods untouched)', () => {
    
      myMockedModule.b.mockImplementationOnce(() => 'overridden');
    
      myModule.a(); // === true
      myModule.b(); // === 'overridden'
    });
    

    Pros

    • Reverts back to original implementation after first call

    Cons

    • It breaks if the test calls b multiple times
    • It doesn't revert to original implementation until b is not called (leaking out in the next test)
  2. jest.doMock(moduleName, factory, options)

    it('should override myModule.b mock result (and leave the other methods untouched)', () => {
    
      jest.doMock('../myModule', () => {
        return {
          a: jest.fn(() => true,
          b: jest.fn(() => 'overridden',
        }
      });
    
      myModule.a(); // === true
      myModule.b(); // === 'overridden'
    });
    

    Pros

    • Explicitly re-mocks on every test

    Cons

    • Cannot define default mock implementation for all tests
    • Cannot extend default implementation forcing to re-declare each mocked method
  3. Manual mocking with setter methods (as explained here)

    // __mocks__/myModule.js
    
    const myMockedModule = jest.genMockFromModule('../myModule');
    
    let a = true;
    let b = true;
    
    myMockedModule.a = jest.fn(() => a);
    myMockedModule.b = jest.fn(() => b);
    
    myMockedModule.__setA = (value) => { a = value };
    myMockedModule.__setB = (value) => { b = value };
    myMockedModule.__reset = () => {
      a = true;
      b = true;
    };
    export default myMockedModule;
    
    // __tests__/myTest.js
    
    it('should override myModule.b mock result (and leave the other methods untouched)', () => {
      myModule.__setB('overridden');
    
      myModule.a(); // === true
      myModule.b(); // === 'overridden'
    
      myModule.__reset();
    });
    

    Pros

    • Full control over mocked results

    Cons

    • Lot of boilerplate code
    • Hard to maintain on long term
  4. jest.spyOn(object, methodName)

    beforeEach(() => {
      jest.clearAllMocks();
      jest.restoreAllMocks();
    });
    
    // Mock myModule
    jest.mock('../myModule');
    
    it('should override myModule.b mock result (and leave the other methods untouched)', () => {
    
      const spy = jest.spyOn(myMockedModule, 'b').mockImplementation(() => 'overridden');
    
      myMockedModule.a(); // === true
      myMockedModule.b(); // === 'overridden'
    
      // How to get back to original mocked value?
    });
    

    Cons

    • I can't revert mockImplementation back to the original mocked return value, therefore affecting the next tests
Corbitt answered 14/2, 2018 at 15:38 Comment(1)
Nice. But how do you do option 2 for a npm module like '@private-repo/module'? Most examples I see have relative paths? Does this work for installed modules as well?Cambric
J
99

A nice pattern for writing tests is to create a setup factory function that returns the data you need for testing the current module.

Below is some sample code following your second example although allows the provision of default and override values in a reusable way.


const spyReturns = returnValue => jest.fn(() => returnValue);

describe("scenario", () => {
  beforeEach(() => {
    jest.resetModules();
  });

  const setup = (mockOverrides) => {
    const mockedFunctions =  {
      a: spyReturns(true),
      b: spyReturns(true),
      ...mockOverrides
    }
    jest.doMock('../myModule', () => mockedFunctions)
    return {
      mockedModule: require('../myModule')
    }
  }

  it("should return true for module a", () => {
    const { mockedModule } = setup();
    expect(mockedModule.a()).toEqual(true)
  });

  it("should return override for module a", () => {
    const EXPECTED_VALUE = "override"
    const { mockedModule } = setup({ a: spyReturns(EXPECTED_VALUE)});
    expect(mockedModule.a()).toEqual(EXPECTED_VALUE)
  });
});

It's important to say that you must reset modules that have been cached using jest.resetModules(). This can be done in beforeEach or a similar teardown function.

See jest object documentation for more info: https://jestjs.io/docs/jest-object.

Jenniejennifer answered 22/2, 2018 at 11:30 Comment(2)
This is actually not working for me. In your case mockedModule is returning mockedModule: typeof jest where .a() is undefined. Instead returning things like advanceTimersByTime, clearMocks, resetAllMocks etc..Topping
@ronnyrr, true, you must use require(...) to obtain mocked module after calling jest.doMock(). Also, you must call jest.resetModules() inside beforeEach() so that your subsequent require(...) calls use the "latest" mocked version of a module. I've submitted an edit about that to the answer to show that in the code too.Uprising
B
137

Use mockFn.mockImplementation(fn).

import { funcToMock } from './somewhere';
jest.mock('./somewhere');

beforeEach(() => {
  funcToMock.mockImplementation(() => { /* default implementation */ });
  // (funcToMock as jest.Mock)... in TS
});

test('case that needs a different implementation of funcToMock', () => {
  funcToMock.mockImplementation(() => { /* implementation specific to this test */ });
  // (funcToMock as jest.Mock)... in TS

  // ...
});
Bellda answered 25/10, 2018 at 17:18 Comment(5)
This worked for me when mocking date-fns-tz's format function.Purport
this should be the accepted answerSeawright
I don't like this answer because it requires duplicating the logic of the function you want to mock. If you change the source function you now have to remember to update every test class that mocks its implementation with its actual logic.Bianca
@Bianca it sounds like Manual Mocks would be suitable for your requirements: jestjs.io/docs/manual-mocks. Feel free to write an answer to this question using them :)Bellda
For TS - the comments marked TS in this answer are absolutely necessary in TS in 2023. I am just pointing that out because I was forever stuck and it's right there. A sample from my usage: import { useTranslation } from 'next-i18next'; and then in beforeEach and individual tests... (useTranslation as jest.Mock).mockImplementation(() => ({Appreciative
J
99

A nice pattern for writing tests is to create a setup factory function that returns the data you need for testing the current module.

Below is some sample code following your second example although allows the provision of default and override values in a reusable way.


const spyReturns = returnValue => jest.fn(() => returnValue);

describe("scenario", () => {
  beforeEach(() => {
    jest.resetModules();
  });

  const setup = (mockOverrides) => {
    const mockedFunctions =  {
      a: spyReturns(true),
      b: spyReturns(true),
      ...mockOverrides
    }
    jest.doMock('../myModule', () => mockedFunctions)
    return {
      mockedModule: require('../myModule')
    }
  }

  it("should return true for module a", () => {
    const { mockedModule } = setup();
    expect(mockedModule.a()).toEqual(true)
  });

  it("should return override for module a", () => {
    const EXPECTED_VALUE = "override"
    const { mockedModule } = setup({ a: spyReturns(EXPECTED_VALUE)});
    expect(mockedModule.a()).toEqual(EXPECTED_VALUE)
  });
});

It's important to say that you must reset modules that have been cached using jest.resetModules(). This can be done in beforeEach or a similar teardown function.

See jest object documentation for more info: https://jestjs.io/docs/jest-object.

Jenniejennifer answered 22/2, 2018 at 11:30 Comment(2)
This is actually not working for me. In your case mockedModule is returning mockedModule: typeof jest where .a() is undefined. Instead returning things like advanceTimersByTime, clearMocks, resetAllMocks etc..Topping
@ronnyrr, true, you must use require(...) to obtain mocked module after calling jest.doMock(). Also, you must call jest.resetModules() inside beforeEach() so that your subsequent require(...) calls use the "latest" mocked version of a module. I've submitted an edit about that to the answer to show that in the code too.Uprising
P
68

Little late to the party, but if someone else is having issues with this.

We use TypeScript, ES6 and babel for react-native development.

We usually mock external NPM modules in the root __mocks__ directory.

I wanted to override a specific function of a module in the Auth class of aws-amplify for a specific test.

    import { Auth } from 'aws-amplify';
    import GetJwtToken from './GetJwtToken';
    ...
    it('When idToken should return "123"', async () => {
      const spy = jest.spyOn(Auth, 'currentSession').mockImplementation(() => ({
        getIdToken: () => ({
          getJwtToken: () => '123',
        }),
      }));

      const result = await GetJwtToken();
      expect(result).toBe('123');
      spy.mockRestore();
    });

Gist: https://gist.github.com/thomashagstrom/e5bffe6c3e3acec592201b6892226af2

Tutorial: https://medium.com/p/b4ac52a005d#19c5

Petard answered 25/1, 2019 at 9:6 Comment(2)
This was the only thing that worked for me, with the least amount of boilerplate. In my scenario I had a named export in TypeScript from a package without a default export, so I ended up using import * as MyModule; and then const { useQuery } = MyModule so I could still use the imports the same way without doing MyModule.someExport everywhere.Gree
medium article mentioned here is gold :)Braille
B
6

When mocking a single method (when it's required to leave the rest of a class/module implementation intact) I discovered the following approach to be helpful to reset any implementation tweaks from individual tests.

I found this approach to be the concisest one, with no need to jest.mock something at the beginning of the file etc. You need just the code you see below to mock MyClass.methodName. Another advantage is that by default spyOn keeps the original method implementation but also saves all the stats (# of calls, arguments, results etc.) to test against, and keeping the default implementation is a must in some cases. So you have the flexibility to keep the default implementation or to change it with a simple addition of .mockImplementation as mentioned in the code below.

The code is in Typescript with comments highlighting the difference for JS (the difference is in one line, to be precise). Tested with Jest 26.6.

describe('test set', () => {
    let mockedFn: jest.SpyInstance<void>; // void is the return value of the mocked function, change as necessary
    // For plain JS use just: let mockedFn;

    beforeEach(() => {
        mockedFn = jest.spyOn(MyClass.prototype, 'methodName');
        // Use the following instead if you need not to just spy but also to replace the default method implementation:
        // mockedFn = jest.spyOn(MyClass.prototype, 'methodName').mockImplementation(() => {/*custom implementation*/});
    });

    afterEach(() => {
        // Reset to the original method implementation (non-mocked) and clear all the mock data
        mockedFn.mockRestore();
    });

    it('does first thing', () => {
        /* Test with the default mock implementation */
    });

    it('does second thing', () => {
        mockedFn.mockImplementation(() => {/*custom implementation just for this test*/});
        /* Test utilising this custom mock implementation. It is reset after the test. */
    });

    it('does third thing', () => {
        /* Another test with the default mock implementation */
    });
});
Bureau answered 15/7, 2021 at 17:22 Comment(2)
If we have concurrency between does second thing and does third thing, the third will use the mockImplementation create by the second :(Tours
You mean multi-threaded test runs, right? Yep, this approach won't work with that as is (the same is true for many other things you put into beforeEach/afterEach). I haven't used this scenario in multithreaded test run setups. Dealing with that is a whole different matter.Bureau
K
2

I needed to solve the same problem in the title, but I use typescript and vitest (same API as Jest). I was stuck for a while until I found this simple API, mocked (jest docs).

// A default export
import getProducts from '../../product-service';
// Or not:
import {getCategories, deleteProduct} from '../../product-service');

vi.mock('../../product-service');

describe('ProductManager', () => {
  it('will not finish when an error is returned', async () => {
    vi.mocked(getProducts).mockRejectedValue(null);
    vi.mocked(getCategories).mockRejectedValue(null);
    // do something and assert the results
  })

  it('will not finish when an error is returned', async () => {
    vi.mocked(getProducts).mockResolvedValue(true);
    // do something and assert the results
  })
}
Kaspar answered 8/6, 2023 at 14:34 Comment(0)
B
1

I did not manage to define the mock inside the test itself so I discover that I could mock several results for the same service mock like this :

jest.mock("@/services/ApiService", () => {
    return {
        apiService: {
            get: jest.fn()
                    .mockResolvedValueOnce({response: {value:"Value", label:"Test"}})
                    .mockResolvedValueOnce(null),
        }
    };
});

I hope it'll help someone :)

Borowski answered 19/4, 2022 at 14:24 Comment(0)
B
1

It's a very cool way I've discovered on this blog https://mikeborozdin.com/post/changing-jest-mocks-between-tests/

import { sayHello } from './say-hello';
import * as config from './config';

jest.mock('./config', () => ({
  __esModule: true,
  CAPITALIZE: null
}));

describe('say-hello', () => {
  test('Capitalizes name if config requires that', () => {
    config.CAPITALIZE = true;

    expect(sayHello('john')).toBe('Hi, John');
  });

  test('does not capitalize name if config does not require that', () => {
    config.CAPITALIZE = false;

    expect(sayHello('john')).toBe('Hi, john');
  });
});
Barbusse answered 3/11, 2022 at 5:57 Comment(0)
P
1

In Jest with Typescript using the jest.mocked api lets us avoid needing to do any casting.

import * as someModule from './somewhere';

jest.mock('./somewhere', () => ({
  functionToMock: jest.fn().mockResolvedValue('defaultValue');
}))

describe('someModule', ()=>{
  it('should return "customValue"',()=>{
    jest.mocked(someModule)
      .functionToMock
      .mockImplementation(jest.fn().mockResolvedValue('customValue');
    
    // Do something that calls `functionToMock` and use the result.
  });
});

Thanks to Ben Butterworth's answer for the inspiration.

Pharyngeal answered 7/7, 2023 at 21:19 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.