Издевательство над классами в шутку не вызывает один и тот же метод

#javascript #node.js #unit-testing #jestjs

#javascript #node.js #модульное тестирование #jestjs

Вопрос:

Я пытаюсь имитировать класс, который импортируется в мой код, require а затем проверяю, вызывается ли метод этого класса.

Я создал пример установки, в которой эта проблема может быть воспроизведена:

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

Ошибка, которую я получаю, выглядит следующим образом:

неудачный тест

Если бы я использовал синтаксис ES6, это работало бы так, как показано в документации jest: https://jestjs.io/docs/en/es6-class-mocks

Но я, к сожалению, не могу использовать ES6 в этом проекте, так как это потребовало бы большого рефакторинга.

Я также пытался издеваться над классом с помощью параметра module factory

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

Он все еще не работает. Когда я вхожу console.log(user.getName) в user-consumer.js:5 систему, это показывает, что метод был подделан, но все, что вызывается, user.getName() не является функцией-потребителем, все равно возвращает «Хан Соло».

Я также пробовал его с и без jest.spyOn , и он по-прежнему возвращает ту же ошибку.

Это просто невозможно с синтаксисом none ES6?

Комментарии:

1. Вы уже используете синтаксис ES6 везде. Если вы ссылаетесь на модули ES, пожалуйста, будьте более конкретными. Проблема никоим образом не относится к ним конкретно.

2. @EstusFlask извиняется. JS на самом деле не моя сильная сторона; Я неправильно понял документы, связанные в вопросе. Я думал, что синтаксис ES6 здесь уже означает использование import . Я говорил, что я пытался использовать синтаксис импорта в этом примере, и это сработало.

Ответ №1:

Проблема в том, что шпионы Jest имеют недокументированное поведение.

Даже если метод прототипа одинаков для всех экземпляров:

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

Шпион специфичен для экземпляра:

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

Если конкретный экземпляр недоступен, это прототип, который необходимо просмотреть:

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

Проблема с jest.mock синтаксисом ES6 не является специфичной. Для того чтобы шпион был доступен для утверждений и изменений реализации, он должен быть где-то открыт. Объявление его вне jest.mock factory не является хорошим решением, поскольку это часто может привести к состоянию гонки, описанному в руководстве; в этом случае тоже будет. Более безопасным подходом является предоставление ссылки как части макета модуля.

Это было бы проще для модуля ES, потому что таким образом экспорт классов сохраняется отдельно:

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

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

Для модуля CommonJS с экспортом класса (функции) он будет эффективно отображаться как статический метод класса:

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

Комментарии:

1. Хорошо, это имеет большой смысл. Я понимаю, что мне действительно нужно было выставить ссылку на издевательскую функцию, но я поступил неправильно — объявил ее вне jest.mock . Который, конечно, не работает, потому jest.mock что выполняется до определения переменной.

2. Для меня работает как использование прототипа, так и предоставление функции, подобной этой, вы рекомендуете использовать метод прототипа? Мне интересно, влияет ли это на последующие тесты, которые могут вызывать этот метод.

3. Другим обходным путем является использование var , которое немного более уродливо и плохо сочетается с __mocks__ любым способом, var mockGetName; jest.mock('./user', () => { mockGetName = jest.fn() ... . Я перечислил оба способа для полноты картины, потому что proto хорошо работает для шпионских / насмешливых методов прототипов. Возможно, вам понадобится jest.mock для моделирования конструктора, включая методы экземпляра (стрелки) .