【问题标题】:How can I mock an ES6 module import using Jest?如何使用 Jest 模拟 ES6 模块导入?
【发布时间】:2021-01-21 02:15:53
【问题描述】:

我想测试我的一个 ES6 模块是否以特定方式调用另一个 ES6 模块。使用Jasmine,这非常简单--

申请代码:

// myModule.js
import dependency from './dependency';

export default (x) => {
  dependency.doSomething(x * 2);
}

以及测试代码:

//myModule-test.js
import myModule from '../myModule';
import dependency from '../dependency';

describe('myModule', () => {
  it('calls the dependency with double the input', () => {
    spyOn(dependency, 'doSomething');

    myModule(2);

    expect(dependency.doSomething).toHaveBeenCalledWith(4);
  });
});

Jest 的等价物是什么?我觉得这是一件很简单的事情,但我一直在努力解决这个问题。

我最接近的方法是将imports 替换为requires,并将它们移动到测试/函数中。这两件事都不是我想做的。

// myModule.js
export default (x) => {
  const dependency = require('./dependency'); // Yuck
  dependency.doSomething(x * 2);
}

//myModule-test.js
describe('myModule', () => {
  it('calls the dependency with double the input', () => {
    jest.mock('../dependency');

    myModule(2);

    const dependency = require('../dependency'); // Also yuck
    expect(dependency.doSomething).toBeCalledWith(4);
  });
});

对于奖励积分,当dependency.js 中的函数是默认导出时,我很乐意让整个事情正常工作。但是,我知道监视默认导出在 Jasmine 中不起作用(或者至少我永远无法让它起作用),所以我也不希望它在 Jest 中是可能的。

【问题讨论】:

  • 我在这个项目中使用 Babel,所以我不介意现在继续将 imports 转换为 requires。不过感谢您的提醒。
  • 如果我有 ts 类 A 并且它调用了一些函数让我们说 B 类的 doSomething() 我们如何模拟,以便 A 类调用 B 类函数 doSomething() 的模拟版本跨度>
  • 对于那些想更多地发现这个问题的人github.com/facebook/jest/issues/936

标签: javascript node.js mocking ecmascript-6 jestjs


【解决方案1】:

你必须自己模拟模块并设置间谍:

import myModule from '../myModule';
import dependency from '../dependency';
jest.mock('../dependency', () => ({
  doSomething: jest.fn()
}))

describe('myModule', () => {
  it('calls the dependency with double the input', () => {
    myModule(2);
    expect(dependency.doSomething).toBeCalledWith(4);
  });
});

【讨论】:

  • 这似乎不对。我得到:babel-plugin-jest-hoist: The second argument of jest.mock must be a function. 所以代码甚至没有编译。
  • 抱歉,我已经更新了我的代码。另请注意,jest.mock 中的路径是相对于测试文件的。
  • 这确实对我有用,但是,在使用默认导出时不起作用。
  • @IrisSchaffer 为了使用默认导出,您需要将 __esModule: true 添加到模拟对象。那是转译后的代码用来判断是转译后的 es6 模块还是 commonjs 模块的内部标志。
  • 模拟默认导出:jest.mock('../dependency', () => ({ default: jest.fn() }))
【解决方案2】:

编辑:几年过去了,这已经不再是正确的方法了(可能从来都不是,我的错)。

对导入的模块进行变异是令人讨厌的,并且可能会导致副作用,例如测试通过或失败取决于执行顺序。

出于历史目的,我将保留此答案的原始形式,但您确实应该使用jest.spyOnjest.mock。有关详细信息,请参阅本页上的笑话文档或其他答案。

原答案如下:


我已经能够通过使用涉及import * 的破解来解决这个问题。它甚至适用于命名和默认导出!

对于命名导出:

// dependency.js
export const doSomething = (y) => console.log(y)
// myModule.js
import { doSomething } from './dependency';

export default (x) => {
  doSomething(x * 2);
}
// myModule-test.js
import myModule from '../myModule';
import * as dependency from '../dependency';

describe('myModule', () => {
  it('calls the dependency with double the input', () => {
    dependency.doSomething = jest.fn(); // Mutate the named export

    myModule(2);

    expect(dependency.doSomething).toBeCalledWith(4);
  });
});

或者对于默认导出:

// dependency.js
export default (y) => console.log(y)
// myModule.js
import dependency from './dependency'; // Note lack of curlies

export default (x) => {
  dependency(x * 2);
}
// myModule-test.js
import myModule from '../myModule';
import * as dependency from '../dependency';

describe('myModule', () => {
  it('calls the dependency with double the input', () => {
    dependency.default = jest.fn(); // Mutate the default export

    myModule(2);

    expect(dependency.default).toBeCalledWith(4); // Assert against the default
  });
});

【讨论】:

  • 这行得通,但它可能不是一个好的做法。对测试范围之外的对象的更改似乎在测试之间持续存在。这可能会在以后导致其他测试中出现意外结果。
  • 您可以使用 jest.spyOn() 而不是使用 jest.fn(),以便稍后恢复原始方法,因此它不会渗入其他测试。我在这里找到了关于不同方法的好文章(jest.fn、jest.mock 和 jest.spyOn):medium.com/@rickhanlonii/understanding-jest-mocks-f0046c68e53c
  • 请注意:如果dependencymyModule 位于同一文件中,则它将不起作用。
  • 我认为这不适用于 Typescript 你正在变异的对象是只读的。
  • 这不适用于在 package.json 中使用type: module 打开的节点实验模块。我让它与 babel 转译器一起工作。
【解决方案3】:

Andreas' answer 添加更多内容。我对 ES6 代码也有同样的问题,但我不想改变导入。那看起来很老套。所以我这样做了:

import myModule from '../myModule';
import dependency from '../dependency';
jest.mock('../dependency');

describe('myModule', () => {
  it('calls the dependency with double the input', () => {
    myModule(2);
  });
});

并在与文件dependency.js平行的“__ mocks __”文件夹中添加了文件dependency.js。这对我有用。此外,这让我可以选择从模拟实现中返回合适的数据。确保为要模拟的模块提供正确的路径。

【讨论】:

  • 谢谢。会试一试。也喜欢这个解决方案 - stackoverflow.com/a/38414160/1882064
  • 我喜欢这种方法的地方在于,它使您能够为要模拟特定模块的所有场合提供一个手动模拟。例如,我有一个翻译助手,它在很多地方都使用过。 __mocks__/translations.js 文件只是默认导出 jest.fn() 类似:export default jest.fn((id) => id)
  • 您也可以使用jest.genMockFromModule 从模块生成模拟。 facebook.github.io/jest/docs/…
  • 需要注意的一点是,通过 export default jest.genMockFromModule('../dependency') 模拟的 ES6 模块在调用 `jest.mock('..dependency') 后会将其所有功能分配给 dependency.default,但在其他方面表现为预计。
  • 你的测试断言是什么样的?这似乎是答案的重要部分。 expect(???)
【解决方案4】:

使用 Jest 模拟 ES6 依赖模块默认导出:

import myModule from '../myModule';
import dependency from '../dependency';

jest.mock('../dependency');

// If necessary, you can place a mock implementation like this:
dependency.mockImplementation(() => 42);

describe('myModule', () => {
  it('calls the dependency once with double the input', () => {
    myModule(2);

    expect(dependency).toHaveBeenCalledTimes(1);
    expect(dependency).toHaveBeenCalledWith(4);
  });
});

其他选项不适用于我的情况。

【讨论】:

  • 如果我只想进行一次测试,最好的清理方法是什么?在每个之后? ```` afterEach(() => { jest.unmock(../dependency'); }) ````
  • @falsarella doMock 在那种情况下真的有效吗?我遇到了非常相似的问题,当我尝试在特定测试中 jest.doMock 时它什么也没做,整个模块的 jest.mock 工作正常
  • @Progress1ve 您也可以尝试将 jest.mock 与 mockImplementationOnce 一起使用
  • 是的,这是一个有效的建议,但是这要求测试是第一个,我不喜欢以这种方式编写测试。我通过导入外部模块并在特定功能上使用 spyOn 解决了这些问题。
  • @Progress1ve 嗯,我的意思是将 mockImplementationOnce 放在每个特定测试中......无论如何,我很高兴你找到了解决方案 :)
【解决方案5】:

我用另一种方式解决了这个问题。假设你有你的dependency.js

export const myFunction = () => { }

我创建了一个 depdency.mock.js 文件,其中包含以下内容:

export const mockFunction = jest.fn();

jest.mock('dependency.js', () => ({ myFunction: mockFunction }));

在测试中,在导入具有依赖关系的文件之前,我使用:

import { mockFunction } from 'dependency.mock'
import functionThatCallsDep from './tested-code'

it('my test', () => {
    mockFunction.returnValue(false);

    functionThatCallsDep();

    expect(mockFunction).toHaveBeenCalled();

})

【讨论】:

  • 这不是有效的玩笑。您将收到如下错误:jest.mock() 的模块工厂不允许引用任何超出范围的变量。
【解决方案6】:

问题已经回答了,但你可以这样解决:

文件 dependency.js

const doSomething = (x) => x
export default doSomething;

文件 myModule.js

import doSomething from "./dependency";

export default (x) => doSomething(x * 2);

文件 myModule.spec.js

jest.mock('../dependency');
import doSomething from "../dependency";
import myModule from "../myModule";

describe('myModule', () => {
  it('calls the dependency with double the input', () => {
    doSomething.mockImplementation((x) => x * 10)

    myModule(2);

    expect(doSomething).toHaveBeenCalledWith(4);
    console.log(myModule(2)) // 40
  });
});

【讨论】:

  • 但是“require”是 CommonJS 语法——OP 询问的是 ES6 模块
  • @Andy 感谢您的评论,我更新了我的答案。顺便说一句,逻辑上也是一样的。
  • 在模拟之前如何在doSomething 上调用.mockImplementation
  • 我认为这个答案需要详细说明,我的问题和上面一样
【解决方案7】:

快进到 2020 年,我发现这篇博文是解决方案:Jest mock default and named export

仅使用 ES6 模块语法:

// esModule.js
export default 'defaultExport';
export const namedExport = () => {};

// esModule.test.js
jest.mock('./esModule', () => ({
  __esModule: true, // this property makes it work
  default: 'mockedDefaultExport',
  namedExport: jest.fn(),
}));

import defaultExport, { namedExport } from './esModule';
defaultExport; // 'mockedDefaultExport'
namedExport; // mock function

还有一件事你需要知道(我花了一段时间才弄清楚)是你不能在测试中调用 jest.mock() ;您必须在模块的顶层调用它。但是,如果您想为不同的测试设置不同的模拟,您可以在单独的测试中调用 mockImplementation()。

【讨论】:

  • 帮助我让它工作的关键是“你不能在测试中调用 jest.mock();你必须在模块的顶层调用它”
  • 您必须在测试顶部有jest.mock 的原因是内部开玩笑将在导入之前重新排序jest.mock。这就是为什么您的jest.mock 是在导入之前还是之后都没有关系的原因。将其放在函数体中,将无法正常运行。
  • __esModule: true 使它在我需要模拟默认导出的地方工作。否则,没有它它工作得很好。感谢您的回答!
  • 我不清楚'mockedDefaultExport' 应该是什么——为什么它不是像mockFunction 这样的变量而不是像'mockFunction' 这样的字符串?为什么不把它们都设为jest.fn()
  • @jcollum 我认为这只是说明任何导出(包括默认导出)都可以像函数一样容易地成为字符串,并且可以以相同的方式模拟跨度>
【解决方案8】:

这里的答案似乎都不适合我,而且 Jest 中的 ESM 支持似乎仍然是 work in progress

在发现this comment 之后,我发现jest.mock() 并不能真正用于常规导入,因为导入总是在模拟之前运行。因此,我使用async import() 导入我的依赖项:

import { describe, expect, it, jest } from '@jest/globals';

jest.mock('../dependency', () => ({
  doSomething: jest.fn()
}));

describe('myModule', async () => {
  const myModule = await import('../myModule');
  const dependency = await import('../dependency');

  it('calls the dependency with double the input', () => {
    myModule(2);
    expect(dependency.doSomething).toBeCalledWith(4);
  });
});

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2021-08-22
    • 2016-05-16
    • 2017-09-08
    • 2017-09-30
    • 2018-05-04
    • 1970-01-01
    相关资源
    最近更新 更多