question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. ItĀ collects links to all the places you might be looking at while hunting down a tough bug.

And, if youā€™re still stuck at the end, weā€™re happy to hop on a call to see how we can help out.

Mocking a module API pains

See original GitHub issue

Firstly, I think jest is a great test runner and I appreciate all the hard work that has gone in to it. Awesome job šŸ‘

However, the mock API does irk me. So Iā€™d like to open this issue to see if thereā€™s something that we can do about it. Some documentation changes may help, some different APIs also may be beneficial.

Do you want to request a feature or report a bug? Feature

What is the current API? Here are some of my pain points with the mocking APIs:

Unclear semantics

The difference in semantics between jest.mock and jest.doMock are not apparent from the names alone, I have to check the docs to remind myself. Same goes for jest.unmock and jest.dontMock. And jest.resetAllMocks and jest.restoreAllMocks. And mockFn.mockReset and mockFn.mockRestore. I think you get the pictureā€¦

jest.mock hoisting

jest.mock calls are hoisted to the top of the scope (via babel-jest). The issues I see here:

  1. Hoisting function calls to the top of the current scope is non-standard JS, itā€™s a language extension. Users probably wonā€™t expect it, which may cause them problems until the new behaviour is learnt. For example:

    // original
    import foo from './foo'
    jest.mock('./foo')
    foo() // is this call mocked?
    
    // transpiled output
    jest.mock('./foo')
    import foo from './foo'
    foo() // yes! it is mocked.
    
  2. It feels like the hoisting was added to fit in with the way existing tests were written, rather than attempting to change the way people write tests to fit the mocking problem. I think TestDouble.js has an interesting solution to this problem. An obvious rip off of their API might look like:

    let mockedFoo = jest.replace('./foo')
    let mockedBar = jest.replace('./bar').default // in this case bar is a default export
    let subject = require('./subject') // subject now depends on mocked foo and bar
    

    The CommonJS (CJS) module format is more flexible for messing around with modules than ES6 modules (ESM). I would suggest keeping both the current API for ESM and introducing the new for CJS tests and in some future version suggest new tests should be written in the CJS style. This way the hoisting behaviour, as well as some of the lesser known caveats of ESM, can be avoided.

Large API surface

The API relating to mocking a module as of v22.4.2 consists of:

  • jest.mock/jest.unmock
  • jest.doMock/jest.dontMock
  • jest.setMock
  • jest.genMockFromModule
  • import * as foo from './foo' + jest.spyOn (See #936)
  • Manual mocks
  • require.requireMock
  • jest.enableAutoMock/jest.disableAutomock

Are all of these different methods of mocking a module required? Can a simpler, smaller API be found to cover the above cases? I donā€™t have any immediate answers but would love to see what the community come up with.


I can probably fix most of these by using a 3rd party lib but it would be nice if Jest made some changes so I didnā€™t have to. I willing to accept that I hold a minority opinion on these APIs and if so, it may not be worth anyones time to work on this. I thought it was worth raising all the same.

Issue Analytics

  • State:closed
  • Created 5 years ago
  • Reactions:29
  • Comments:23 (9 by maintainers)

github_iconTop GitHub Comments

34reactions
nfantonecommented, Jun 22, 2018

I second this. jest mock API is convoluted, at best, and generally confusing, IMHO.

  • Do I need to reset, restore or clear a mock?
  • Why do restore work only on spies?
  • Is it a module or a mock that needs restoring?
  • Do I use genMockFromModule or plain .mock to mock a module?
  • Was it .mock that gets automatically hoistedā€¦ or is it doMock?
  • Why is there an unmock?
  • How do I partially mock a moduleā€™s API?
  • Why is it not straightforward to declare mocks per test?

Iā€™m not saying these questions donā€™t have proper answers. Iā€™m making the point that itā€™s the fact that you need to consider those answers and think about them every time you (or at least I) write a test using jest. I end up losing myself in the docs and/or SO more than I feel I should.

15reactions
aaronjensencommented, Jun 26, 2018

Hi all, Iā€™ve been using jest for quite a while now. I agree with OP that the runner itself is fantastic. That said, Iā€™ve tried a number of times over the many projects Iā€™ve used jest on to use jest mocks. I always run into confusion and problems. Itā€™s very possible that Iā€™m doing something wrong and have some fundamental misunderstanding, but the current API doesnā€™t appear to lead to the pit of success.

I think that the problems Iā€™ve run into boil down to a simple fact: jest mocks are not generally safe to use between tests. That is, they leak state between tests by default. As far as I can tell, they leak recordings and implementations. If I mockReturnValue in a test then the next test will get that return value too if I donā€™t manually restore the mock.

This plays poorly with some of the other features of jest like manual mocks. Hereā€™s an example of something Iā€™m trying to do right now that led me to this issue.

__mocks__/service.js

export default {
  foo: jest.fn(() => 'original')
}

__tests__/mytest.js

import service from '../service'

it('original', () => {
  expect(service.foo()).toEqual('original')
})

it('override', () => {
  trelloService.foo.mockReturnValue('override')
  expect(service.foo()).toEqual('override')
})

it('original', () => {
  // none of these matter:
  // jest.restoreAllMocks()
  // jest.resetAllMocks()
  // jest.clearAllMocks()
  expect(service.foo()).toEqual('original') // FAIL
})

So, I know Iā€™m doing something ā€œwrongā€ here. I probably shouldnā€™t be using manual mocks in these ways. However, the problem doesnā€™t end with manual mocks. Many of the jest examples in the docs show mocks being created at the module level. This fails too:

const foo = jest.fn(() => 'original')

it('original', () => {
  expect(foo()).toEqual('original')
})

it('override', () => {
  foo.mockReturnValue('override')
  expect(foo()).toEqual('override')
})

it('original', () => {
  expect(foo()).toEqual('original') // FAIL
})

Furthermore, the necessity for jest.mock to be at the module level makes it more likely that jest.fn()'s will be created at the module level. Basically, every time Iā€™ve used jest Iā€™ve felt pushed to create mocks at the module level only to find that, like the north, mocks remember.

What I think would be fantastic is if:

  1. There was a single function to completely reset mocks to the state they were in when they were created.
  2. That function worked on jest.fn()'s created at the module level (or in manual mocks).
  3. That function was called between tests by default.

I believe, if those were true, then mocks would ā€œjust workā€ and wouldnā€™t leak state between tests.

The API mentioned above looks nicer, but, at least for me, this is the much bigger pain point.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Mocking External APIs in Python
The following tutorial demonstrates how to test the use of an external API using Python mock objects. Integrating with a third-party application is...
Read more >
Avoid the pain of mocking modules with dependency injection
Before we get into dependency Injection (DI), let's see how mocking modules causes so much pain. ... A mock from one test creeping...
Read more >
The only 3 steps you need to mock an API call in Jest
To mock an API call in a function, you just need to do these 3 steps: 1. Import the module you want to...
Read more >
How To Do Manual Mock In Jest Unit Testing | by Kris Ma
Tell jest to use the above code to mock the module we want. The typical syntax of jest manual mock is. jest.mock(moduleName, factory,...
Read more >
Mock all functions in a JavaScript module except one using Jest
Auto-mocking means that Jest will replace any function within the mocked module ( ../utils/players in this case) with a new function that justĀ ......
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found