如何使用Jest模拟ES6模块导入?

我想测试我的ES6模块中的一个以特定的方式调用另一个ES6模块。使用茉莉花,这非常简单——

应用程序代码:

// 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对应的是什么?我觉得这是一件很简单的事情,但我一直在努力想弄清楚。

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

// 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中也能做到这一点不抱希望。

389337 次浏览

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

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

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

修改导入的模块非常麻烦,可能会导致副作用,比如根据执行顺序通过或失败的测试。

出于历史原因,我保留了这个答案的原始形式,但你真的应该使用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
});
});


安德烈亚斯的回答添加更多。我在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的文件夹。这对我很管用。此外,这让我可以选择从模拟实现中返回合适的数据。确保为要模拟的模块提供了正确的路径。

使用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);
});
});

其他的选择对我的案子不起作用。

我用另一种方法解决了这个问题。假设你有依赖项。js

export const myFunction = () => { }

我在它之外创建了一个dependency .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();


})

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

文件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
});
});

快进到2020年,我发现这篇博客文章是解决方案:笑话模拟默认和命名导出

仅使用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

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

这里的答案似乎都不适合我(原来的函数总是被导入而不是mock),而且似乎ESM支持在Jest仍然是进行中的工作

在发现这样的评论之后,我发现jest.mock()实际上并不适用于常规导入,因为导入总是在模拟之前运行。因此,我使用await import()导入依赖项。这甚至适用于顶级的等待,所以我只需要调整我的导入:

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


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


const myModule = await import('../myModule');
const dependency = await import('../dependency');


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

我尝试了所有的解决方案,没有一个工作或显示大量的TS错误。

我是这样解决的:

format.ts文件:

import camelcaseKeys from 'camelcase-keys'
import parse from 'xml-parser'


class Format {
parseXml (xml: string) {
return camelcaseKeys(parse(xml), {
deep: true,
})
}
}


const format = new Format()
export { format }

format.test.ts文件:

import format from './format'
import camelcaseKeys from 'camelcase-keys'
import parse from 'xml-parser'


jest.mock('xml-parser', () => jest.fn().mockReturnValue('parsed'))
jest.mock('camelcase-keys', () => jest.fn().mockReturnValue('camel cased'))


describe('parseXml', () => {
test('functions called', () => {
const result = format.parseXml('XML')


expect(parse).toHaveBeenCalledWith('XML')
expect(camelcaseKeys).toHaveBeenCalledWith('parsed', { deep: true })
expect(result).toBe('camel cased')
})
})

我对@cam-jackson的原始答案做了一些修改,副作用已经消失了。我使用lodash库深度克隆测试对象,然后对该对象进行任何修改。但是要注意克隆沉重的对象会对测试性能和测试速度产生负面影响。

objectUndertest.js

const objectUnderTest = {};
export default objectUnderTest;


objectUnderTest.myFunctionUnterTest = () => {
return "this is original function";
};

objectUndertest.test.js

import _ from "lodash";
import objectUndertest from "./objectUndertest.js";


describe("objectUndertest", () => {
let mockObject = objectUndertest;


beforeEach(() => {
mockObject = _.cloneDeep(objectUndertest);
});


test("test function", () => {
mockObject.myFunctionUnterTest = () => {
return "this is mocked function.";
};


expect(mockObject.myFunctionUnterTest()).toBe("this is mocked function.");
});
});