jest.mock():如何使用工厂参数模拟 ES6 类默认导入

Posted

技术标签:

【中文标题】jest.mock():如何使用工厂参数模拟 ES6 类默认导入【英文标题】:jest.mock(): How to mock ES6 class default import using factory parameter 【发布时间】:2018-05-04 06:17:13 【问题描述】:

模拟 ES6 类导入

我想在我的测试文件中模拟我的 ES6 类导入。

如果被模拟的类有多个消费者,将模拟移动到 __mocks__ 可能是有意义的,这样所有测试都可以共享模拟,但在此之前我想将模拟保留在测试文件中。

Jest.mock()

jest.mock() 可以模拟导入的模块。传递单个参数时:

jest.mock('./my-class.js');

它使用与模拟文件相邻的 __mocks__ 文件夹中的模拟实现,或创建一个自动模拟。

模块出厂参数

jest.mock() 接受一个第二个参数,它是一个模块工厂 函数。 对于使用export default 导出的 ES6 类,不清楚这个工厂函数应该返回什么。是:

    另一个返回模拟类实例的对象的函数? 模仿类实例的对象? 具有属性default 的对象是一个返回模拟类实例的对象的函数? 返回高阶函数的函数,该高阶函数本身返回 1、2 或 3?

The docs 很模糊:

第二个参数可用于指定正在运行的显式模块工厂,而不是使用 Jest 的自动模拟功能:

当消费者imports 类时,我正在努力想出一个可以作为构造函数的工厂定义。我不断收到TypeError: _soundPlayer2.default is not a constructor(例如)。

我尝试避免使用箭头函数(因为它们不能用new 调用)并让工厂返回一个具有default 属性的对象(或没有)。

这是一个例子。这是行不通的;所有测试都抛出TypeError: _soundPlayer2.default is not a constructor

正在测试的类: sound-player-consumer.js

import SoundPlayer from './sound-player'; // Default import

export default class SoundPlayerConsumer 
  constructor() 
    this.soundPlayer = new SoundPlayer(); //TypeError: _soundPlayer2.default is not a constructor
  

  playSomethingCool() 
    const coolSoundFileName = 'song.mp3';
    this.soundPlayer.playSoundFile(coolSoundFileName);
  

类被嘲笑: sound-player.js

export default class SoundPlayer 
  constructor() 
    // Stub
    this.whatever = 'whatever';
  

  playSoundFile(fileName) 
    // Stub
    console.log('Playing sound file ' + fileName);
  

测试文件:sound-player-consumer.test.js

import SoundPlayerConsumer from './sound-player-consumer';
import SoundPlayer from './sound-player';

// What can I pass as the second arg here that will 
// allow all of the tests below to pass?
jest.mock('./sound-player', function()  
  return 
    default: function() 
      return 
        playSoundFile: jest.fn()
      ;
    
  ;
);

it('The consumer should be able to call new() on SoundPlayer', () => 
  const soundPlayerConsumer = new SoundPlayerConsumer();
  expect(soundPlayerConsumer).toBeTruthy(); // Constructor ran with no errors
);

it('We can check if the consumer called the mocked class constructor', () => 
  const soundPlayerConsumer = new SoundPlayerConsumer();
  expect(SoundPlayer).toHaveBeenCalled();
);

it('We can check if the consumer called a method on the class instance', () => 
  const soundPlayerConsumer = new SoundPlayerConsumer();
  const coolSoundFileName = 'song.mp3';
  soundPlayerConsumer.playSomethingCool();
  expect(SoundPlayer.playSoundFile).toHaveBeenCalledWith(coolSoundFileName);
);

我可以将什么作为第二个参数传递给 jest.mock() 以允许示例中的所有测试通过?如果需要修改测试也没关系 - 只要它们仍然测试相同的东西。

【问题讨论】:

【参考方案1】:

感谢feedback from @SimenB on GitHub.,更新了解决方案


工厂函数必须返回一个函数

工厂函数必须返回模拟对象:代替模拟对象的对象。

由于我们要模拟一个 ES6 类,即a function with some syntactic sugar,那么模拟本身必须是一个函数。因此传递给jest.mock() 的工厂函数必须返回一个函数;换句话说,它必须是一个高阶函数。

在上面的代码中,工厂函数返回一个对象。由于在对象上调用new 失败,所以它不起作用。

你可以调用new的简单模拟:

这是一个简单的版本,因为它返回一个函数,所以允许调用new

jest.mock('./sound-player', () => 
  return function() 
    return  playSoundFile: () =>  ;
  ;
);

注意:箭头功能不起作用

请注意,我们的 mock 不能是箭头函数,因为我们不能在 javascript 中对箭头函数调用 new;这是语言固有的。所以这行不通:

jest.mock('./sound-player', () => 
  return () =>  // Does not work; arrow functions can't be called with new
    return  playSoundFile: () =>  ;
  ;
);

这将抛出 TypeError: _soundPlayer2.default is not a constructor

跟踪使用情况(监视模拟)

不抛出错误很好,但是我们可能需要测试我们的构造函数是否被正确的参数调用。

为了跟踪对构造函数的调用,我们可以将 HOF 返回的函数替换为 Jest 模拟函数。我们用jest.fn() 创建它,然后用mockImplementation() 指定它的实现。

jest.mock('./sound-player', () => 
  return jest.fn().mockImplementation(() =>  // Works and lets you check for constructor calls
    return  playSoundFile: () =>  ;
  );
);

这将让我们使用 SoundPlayer.mock.calls 检查模拟类的使用情况。

监视我们类的方法

我们的模拟类将需要提供将在我们的测试期间调用的任何成员函数(示例中为playSoundFile),否则我们会因为调用不存在的函数而出错。但我们可能还想监视对这些方法的调用,以确保使用预期的参数调用它们。

因为在我们的测试过程中将创建一个新的模拟对象,SoundPlayer.playSoundFile.calls 不会帮助我们。为了解决这个问题,我们用另一个模拟函数填充playSoundFile,并将对相同模拟函数的引用存储在我们的测试文件中,以便我们可以在测试期间访问它。

let mockPlaySoundFile = jest.fn();
jest.mock('./sound-player', () => 
  return jest.fn().mockImplementation(() =>  // Works and lets you check for constructor calls
    return  playSoundFile: mockPlaySoundFile ; // Now we can track calls to playSoundFile
  );
);

完整示例

这是它在测试文件中的样子:

import SoundPlayerConsumer from './sound-player-consumer';
import SoundPlayer from './sound-player';

let mockPlaySoundFile = jest.fn();
jest.mock('./sound-player', () => 
  return jest.fn().mockImplementation(() => 
    return  playSoundFile: mockPlaySoundFile ;
  );
);

it('The consumer should be able to call new() on SoundPlayer', () => 
  const soundPlayerConsumer = new SoundPlayerConsumer();
  expect(soundPlayerConsumer).toBeTruthy(); // Constructor ran with no errors
);

it('We can check if the consumer called the class constructor', () => 
  const soundPlayerConsumer = new SoundPlayerConsumer();
  expect(SoundPlayer).toHaveBeenCalled();
);

it('We can check if the consumer called a method on the class instance', () => 
  const soundPlayerConsumer = new SoundPlayerConsumer();
  const coolSoundFileName = 'song.mp3';
  soundPlayerConsumer.playSomethingCool();
  expect(mockPlaySoundFile.mock.calls[0][0]).toEqual(coolSoundFileName);
);

【讨论】:

我仍然收到TypeError: ...default is not a constructor 如果您向我们提供一些有关您在遇到该错误时的具体操作的详细信息,我或社区中的其他人可能会为您提供帮助。也许开始一个新问题?此文档也可能有所帮助:facebook.github.io/jest/docs/en/es6-class-mocks.html 一些看起来微不足道的东西导致构造函数在模拟后缺乏定义 - ES6 类不是默认导出。在我将模拟类更新为默认导出然后分别更新导入之前,无法进行任何工作。之后模拟工作。 你会如何用不同的输入模拟 playSoundFile: mockPlaySoundFile?就像在一种情况下它返回期望值一样,在第二种情况下它会抛出错误。我们需要一个单独的测试文件吗? @valearner 在这种情况下,您可以覆盖测试用例中的模拟或链接 mockReturnValueOnce() 调用。请参阅jestjs.io/docs/en/… 了解更多信息。【参考方案2】:

如果您仍然收到 TypeError: ...default is not a constructor 并且正在使用 TypeScript,请继续阅读。

TypeScript 正在转译您的 ts 文件,并且您的模块很可能是使用 ES2015s import 导入的。 const soundPlayer = require('./sound-player')。 因此,创建作为默认导出的类的实例将如下所示: new soundPlayer.default()。 但是,如果您按照文档的建议模拟课程。

jest.mock('./sound-player', () => 
  return jest.fn().mockImplementation(() => 
    return  playSoundFile: mockPlaySoundFile ;
  );
);

你会得到同样的错误,因为soundPlayer.default 没有指向一个函数。 您的模拟必须返回一个对象,该对象具有指向函数的默认属性。

jest.mock('./sound-player', () => 
    return 
        default: jest.fn().mockImplementation(() => 
            return 
                playSoundFile: mockPlaySoundFile 
               
        )
    
)

对于命名导入,如import OAuth2 from './oauth',在此示例中将default 替换为导入的模块名称OAuth2

jest.mock('./oauth', () => 
    return 
        OAuth2: ... // mock here
    
)

【讨论】:

非常感谢!就我而言,我使用的是命名导入import OAuth2 from '...'。我刚刚用OAuth2: 替换了你的答案中的default:,它起作用了! 谢谢@seebiscuit,很高兴我的评论有所帮助。但是 nidkil 的回答已经在他的 repo 中涵盖了命名导入:github.com/nidkil/jest-test/blob/master/test/es6-classes/named/… @MaximMazurok 从许多标准来看,链接不是答案。在未来,回购可能不存在。此外,目前尚不完全清楚正确的解决方案可能在回购中的哪个位置。高质量的答案包括帖子中的相关信息。【参考方案3】:

Stone 和 Santiago 帮我解决了这个问题。我只想提一下,此外,我必须在我的 import 语句之前加上 jest mock 函数,如下所示:

jest.mock('bootstrap/dist/js/bootstrap.esm.js', () => 
    return 
        Tooltip: function(init)
            this.init = init;
        
    
)

import  newSpecPage  from '@stencil/core/testing';
import  CoolCode  from '../cool-code';

感谢您的帮助!

【讨论】:

【参考方案4】:

如果你定义了一个模拟类,你可以使用类似的东西:

jest.mock("../RealClass", () => 
  const mockedModule = jest.requireActual(
    "../path-to-mocked-class/MockedRealClass"
  );
  return 
    ...mockedModule,
  ;
);

代码会做一些事情,比如用 MockedRealClass 替换原始 RealClass 的方法和属性定义。

【讨论】:

上面的代码实际上与让 jest 自动模拟 RealClass 相同,但更多的代码没有增加任何价值。 向下滚动到@stone 的答案,详细了解手动模拟 ES6 类的方式和原因。 @99linesofcode "stone 的代码对我不起作用,但我发布的代码对我的情况有效。 如果你想自动模拟一个模块,这基本上就是你在这里所做的一切,你可以简单地将代码替换为 jest.mock("../RealClass"); @99linesofcode 我认为您不明白“../path-to-mocked-class/MockedRealClass”背后的想法,这是您放置“模拟类”的地方,它描述了REAL 类的一些功能。它不是真正的类。它是定义方法的文件,这些方法只是模仿真实的类方法。并且是由开发者编写的,不是自动生成的,不是自动模拟的。

以上是关于jest.mock():如何使用工厂参数模拟 ES6 类默认导入的主要内容,如果未能解决你的问题,请参考以下文章

使用 jest.mock('axios') 时如何模拟拦截器?

如何使用 jest.mock 模拟 useRef 和反应测试库

笑话模拟工厂不适用于模拟类

ES6类开玩笑嘲弄

在jest.mock中Jest'TypeError:不是函数'

如何模拟特定功能而不影响同一个ES6类中的其他功能?