简体   繁体   中英

Mocking classes in Jest does not call the same method

I'm trying to mock a class that is being imported into my code with require and then testing if a method of that class is getting called.

I've created a sample setup where this issue can be replicated:

// user.js
class User {
  getName() {
    return "Han Solo"
  }
}

module.exports = User
// user-consumer.js
const User = require('./user')
const user = new User()

module.exports.getUserName = () => {
  // do things here
  return user.getName() 
}
// user.test.js
const userConsumer = require('./user-consumer')
const User = require('./user')
jest.mock('./user')

it('should mock', () => {
  const user = new User()
  jest.spyOn(user, 'getName')
  userConsumer.getUserName()
  expect(user.getName).toBeCalled()
})

The error I get is as follows:

失败的测试

If I used ES6 syntax this would work as shown on jest's documentation: https://jestjs.io/docs/en/es6-class-mocks

But I unfortunately can't use ES6 on this project as it would require a lot of refactoring.

I also tried mocking the class with the module factory parameter

jest.mock('./user', () => {
  return jest.fn(() => {
    return {
      getName: jest.fn(),
    }
  })
})

It still doesn't work. When I log console.log(user.getName) in user-consumer.js:5 it does show that the method has been mocked but whatever is called in user.getName() is not the consumer function still returns "Han Solo".

I've also tried it with and without jest.spyOn and it still returns the same error.

Is this just not possible with none ES6 syntax?

The problem is that Jest spies have undocumented behaviour.

Even if prototype method is the same for all instances:

new User().getName === new User().getName

A spy is specific to an instance:

jest.spyOn(new User(), 'getName') !== jest.spyOn(new User(), 'getName') 

If a specific instance is unreachable, it's a prototype that needs to be spied:

jest.spyOn(User.prototype, 'getName')
userConsumer.getUserName()
expect(User.prototype.getName).toBeCalled();

A problem with jest.mock isn't specific to ES6 syntax. In order for a spy to be available for assertions and implementation changes, it should be exposed somewhere. Declaring it outside jest.mock factory is not a good solution as it can often result in race condition described in the manual ; there will be one in this case too. A more safe approach is to expose a reference as a part of module mock.

It would be more straightforward for ES module because this way class export is kept separately:

import MockedUser, { mockGetName } from './user';

jest.mock('./user', () => {
  const mockGetName = jest.fn();
  return {
    __esModule: true,
    mockGetName,
    default: jest.fn(() => {
      return {
        getName: mockGetName
      }
    })
  }
})
...

For CommonJS module with class (function) export, it will be efficiently exposed as class static method:

import MockedUser from './user';

jest.mock('./user', () => {
  const mockGetName = jest.fn();
  return Object.assign(
    jest.fn(() => {
      return {
        getName: mockGetName
      }
    }),
    { mockGetName }
  })
})
...
MockedUser.mockGetName.mockImplementation(...);
userConsumer.getUserName()
expect(MockedUser.mockGetName).toBeCalled();

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM