Допустим, у вас есть файл:

// f.js
export function b(){
  return 'b';
}
export function a(){
  return b();
}

Если вы хотите издеваться над b, чтобы проверить a, что ж ... Это не так просто, как кажется.

Наивный подход

Первоначально я пытался сделать следующее:

test('a', () => {
  const f = require('./f');
  jest.spyOn(f, 'b').mockReturnValue('c');
  expect(f.a()).toBe('c');
  // FAILED!
  // expected 'c' got 'b'
})

Нет ... Не пойдет.

Это связано с тем, что экспортированный b действительно является издевательским, но это не тот b, который вызывается a внутри модуля.

Решение 1. Разделение модуля на разные файлы

Если вы переместите b в отдельный файл:

// b.js
export function b(){
  return 'b';
}
// f.js
import {b} from './b';
export function a(){
  return b();
}

Тогда тест пройдёт:

test('a', () => {
  const b = require('./b');
  const f = require('./f');
  jest.spyOn(b, 'b').mockReturnValue('c');
  expect(f.a()).toBe('c');
  //PASSED!
})

Это выглядит как самое чистое решение, но что, если вы хотите сохранить свои функции в одном файле?

Решение 2. Вызов имитационной функции с использованием экспорта

Добавьте exports. перед вызовом функции

// f.js
export function b(){
  return 'b';
}
export function a(){
  return exports.b();
}

И тогда тест просто пройдет:

test('a', () => {
  const f = require('./f');
  jest.spyOn(f, 'b').mockReturnValue('c');
  expect(f.a()).toBe('c');
  //PASSED!
})

Мне очень нравится это решение, потому что в тестируемом файле вы меняете только что-то несущественное. Более того, даже этого небольшого изменения можно избежать:

Вы можете использовать библиотеку babel-plugin-explicit-exports-references, чтобы программно добавить exports. ко всем функциям в одном модуле.

Обратите внимание, что библиотека очень свежая и имеет очень небольшую аудиторию.
Используйте ее с осторожностью с точки зрения безопасности.

Стоит упомянуть неоптимальное решение - экспорт объекта пространства имен

Вы можете создать пространство имен, которое экспортируете как объект по умолчанию, и вызовите b, используя пространство имен.

Таким образом, когда вы вызываете jest.mock, он заменяет функцию b в объекте пространства имен.

// f.js
const f = {
  b(){
    return 'b';
  },
  a(){
    return f.b();
  }
};
export default f;

И тогда тест пройдёт:

test('a', () => {
  const f = require('./f');
  jest.spyOn(f, 'b').mockReturnValue('c');
  expect(f.a()).toBe('c');
  //PASSED!
})

Но тогда вам нужно будет импортировать его как default импорт, не имея возможности разбить его на именованные экспорты.

// won't work:
// import {a} from './f';
import f from './f';
...
f.a();

Что довольно некрасиво.

Другое неоптимальное решение - использование библиотеки переподключения

Библиотека babel-plugin-rewire - это навязчивая библиотека, которая меняет то, что находится внутри модулей.

Кажется, это не очень поддерживаемая библиотека, и на самом деле она не сработала для меня, потому что не поддерживает TypeScript, но вот примерно то, как она должна работать:

Вам не нужно ничего менять в тестируемом файле.

// f.js
export function b(){
  return 'b';
}
export function a(){
  return b();
}

Перепрограммируйте функцию b в тесте:

import {a, __set__} from './f';
test('a', () => {
  const f = require('./f');
  // This rewires b to return 'c'
  __set__('b', () => 'c');
  expect(f.a()).toBe('c');
  // PASSED!
})

Удачного тестирования :)