如何使用 Jest 模拟同一模块中的函数?

正确模仿以下示例的最佳方法是什么?

问题是,在导入时间之后,foo将保留对原始非模拟 bar的引用。

返回文章页面

export function bar () {
return 'bar';
}


export function foo () {
return `I am foo. bar is ${bar()}`;
}

返回文章页面

import * as module from '../src/module';


describe('module', () => {
let barSpy;


beforeEach(() => {
barSpy = jest.spyOn(
module,
'bar'
).mockImplementation(jest.fn());
});




afterEach(() => {
barSpy.mockRestore();
});


it('foo', () => {
console.log(jest.isMockFunction(module.bar)); // outputs true


module.bar.mockReturnValue('fake bar');


console.log(module.bar()); // outputs 'fake bar';


expect(module.foo()).toEqual('I am foo. bar is fake bar');
/**
* does not work! we get the following:
*
*  Expected value to equal:
*    "I am foo. bar is fake bar"
*  Received:
*    "I am foo. bar is bar"
*/
});
});

我可以改变:

export function foo () {
return `I am foo. bar is ${bar()}`;
}

致:

export function foo () {
return `I am foo. bar is ${exports.bar()}`;
}

但在我看来,到处都这样做是相当丑陋的。

51276 次浏览

The problem seems to be related to how you expect the scope of bar to be resolved.

一方面,在 module.js中导出两个函数(而不是包含这两个函数的对象)。由于导出模块的方式,对导出事物的容器的引用是 exports,就像您提到的那样。

另一方面,您处理您的导出(别名为 module)就像一个持有这些函数并试图替换其中一个函数(函数栏)的对象。

如果仔细观察 foo 实现,您实际上持有对 bar 函数的固定引用。

当您认为用一个新的函数替换 bar 函数时,您实际上只是替换了 module.test.js 作用域中的引用副本

要使 foo 实际上使用另一个版本的 bar,你有两种可能性:

  1. 在 module.js 中,导出一个包含 foo 和 bar 方法的类或实例:

    Module.js:

    export class MyModule {
    function bar () {
    return 'bar';
    }
    
    
    function foo () {
    return `I am foo. bar is ${this.bar()}`;
    }
    }
    

    注意 foo 方法中使用的 这个关键字

    Js:

    import { MyModule } from '../src/module'
    
    
    describe('MyModule', () => {
    //System under test :
    const sut:MyModule = new MyModule();
    
    
    let barSpy;
    
    
    beforeEach(() => {
    barSpy = jest.spyOn(
    sut,
    'bar'
    ).mockImplementation(jest.fn());
    });
    
    
    
    
    afterEach(() => {
    barSpy.mockRestore();
    });
    
    
    it('foo', () => {
    sut.bar.mockReturnValue('fake bar');
    expect(sut.foo()).toEqual('I am foo. bar is fake bar');
    });
    });
    
  2. Like you said, rewrite the global reference in the global exports container. This is not a recommended way to go as you will possibly introduce weird behaviors in other tests if you don't properly reset the exports to its initial state.

我选择的解决方案是通过设置默认参数使用 dependency injection

所以我会改变

export function bar () {
return 'bar';
}


export function foo () {
return `I am foo. bar is ${bar()}`;
}

export function bar () {
return 'bar';
}


export function foo (_bar = bar) {
return `I am foo. bar is ${_bar()}`;
}

This is not a breaking change to the API of my component, and I can easily override bar in my test by doing the following

import { foo, bar } from '../src/module';


describe('module', () => {
it('foo', () => {
const dummyBar = jest.fn().mockReturnValue('fake bar');
expect(foo(dummyBar)).toEqual('I am foo. bar is fake bar');
});
});

这样做的好处是可以得到稍微好一点的测试代码:)

另一种解决方案是将模块导入到自己的代码文件中,并使用所有导出实体的导入实例。像这样:

import * as thisModule from './module';


export function bar () {
return 'bar';
}


export function foo () {
return `I am foo. bar is ${thisModule.bar()}`;
}

现在模仿 bar真的很容易,因为 foo也使用了导出的 bar实例:

import * as module from '../src/module';


describe('module', () => {
it('foo', () => {
spyOn(module, 'bar').and.returnValue('fake bar');
expect(module.foo()).toEqual('I am foo. bar is fake bar');
});
});

将模块导入到它自己的代码中看起来很奇怪,但是由于 ES6支持循环导入,所以它的工作非常顺利。

如果定义了导出,那么可以将函数作为导出对象的一部分引用。然后您可以分别覆盖您的模拟中的函数。这是由于导入作为引用的工作方式,而不是作为副本。

Module.js:

exports.bar () => {
return 'bar';
}


exports.foo () => {
return `I am foo. bar is ${exports.bar()}`;
}

Test.js:

describe('MyModule', () => {


it('foo', () => {
let module = require('./module')
module.bar = jest.fn(()=>{return 'fake bar'})


expect(module.foo()).toEqual('I am foo. bar is fake bar');
});


})

我也有同样的问题,由于项目的链接标准,定义类或者重写 exports中的引用即使不受链接定义的阻碍,也不是代码审查批准的选项。我偶然发现的一个可行的选择是使用 Babel-rewire-plugin,这是更干净,至少在外观。虽然我发现这在另一个项目中使用,我有访问,我注意到它已经在一个类似的问题,我已经链接 给你的答案。这是一个针对这个问题(不使用间谍)进行了调整的片段,它来自于链接答案,以供参考(我除了删除间谍之外还添加了分号,因为我不是异教徒) :

import __RewireAPI__, * as module from '../module';


describe('foo', () => {
it('calls bar', () => {
const barMock = jest.fn();
__RewireAPI__.__Rewire__('bar', barMock);
    

module.foo();


expect(bar).toHaveBeenCalledTimes(1);
});
});

Https://stackoverflow.com/a/45645229/6867420

对我有用:

cat moduleWithFunc.ts


export function funcA() {
return export.funcB();
}
export function funcB() {
return false;
}


cat moduleWithFunc.test.ts


import * as module from './moduleWithFunc';


describe('testFunc', () => {
beforeEach(() => {
jest.clearAllMocks();
});


afterEach(() => {
module.funcB.mockRestore();
});


it.only('testCase', () => {
// arrange
jest.spyOn(module, 'funcB').mockImplementationOnce(jest.fn().mockReturnValue(true));


// act
const result = module.funcA();


// assert
expect(result).toEqual(true);
expect(module.funcB).toHaveBeenCalledTimes(1);
});
});

如果您使用 Babel (即 @babel/parser)来处理翻译代码,babel-plugin-explicit-exports-references1 npm 包通过为您制作“丑陋的”module.exports替代品来非常优雅地解决这个问题。有关更多信息,请参见 the original problem thread


注意: 这个插件是我写的!

对于 CommonJS 模块用户,假设文件类似于:

/* myModule.js */
function bar() {
return "bar";
}


function foo() {
return `I am foo. bar is ${bar()}`;
}


module.exports = { bar, foo };

您需要将该文件修改为:

/* myModule.js */
function bar() {
return "bar";
}


function foo() {
return `I am foo. bar is ${myModule.bar()}`;  // Change `bar()` to `myModule.bar()`
}


const myModule = { bar, foo };  // Items you wish to export


module.exports = myModule;  // Export the object

您的原始测试套件(myModule.test.js)现在应该通过:

const myModule = require("./myModule");


describe("myModule", () => {
test("foo", () => {
jest.spyOn(myModule, "bar").mockReturnValueOnce("bar-mock");


const result = myModule.foo();
expect(result).toBe("I am foo. bar is bar-mock");
});
});

阅读更多: 在 Jest 的单个模块中导出函数

这里有各种各样的方法可以使这个工作,但是大多数人应该使用的真正的答案是: 不要。以 OP 的示例模块为例:

export function bar () {
return 'bar';
}


export function foo () {
return `I am foo. bar is ${bar()}`;
}

测试 实际行为,你会写:

import { bar, foo } from "path/to/module";


describe("module", () => {
it("foo returns 'bar'", () => {
expect(bar()).toBe('bar');
});


it("foo returns 'I am foo. bar is bar'", () => {
expect(foo()).toBe('I am foo. bar is bar');
});
});

Why? Because then you can refactor inside the module boundary without changing the tests, which gives you the confidence to improve the quality of your code in the knowledge that it still does what it's supposed to.

假设您将 'bar'的创建从 bar提取到一个未导出的函数,例如:

function rawBar() {
return 'bar';
}


export function bar () {
return rawBar();
}


export function foo () {
return `I am foo. bar is ${rawBar()}`;
}

我建议上述测试将通过。如果您断言调用 foo意味着调用 bar,那么测试将开始失败,即使重构保留了模块的行为(相同的 API,相同的输出)。那是 实施细节

测试双精度模块是针对 通敌者的,如果确实需要在这里对某些内容进行模拟,那么应该将其提取到一个单独的模块中(这样模拟就容易得多,这表明您正在朝着正确的方向前进)。尝试在同一个模块中模仿函数就像模仿要测试的类的某些部分一样,我在这里进行了类似的说明: https://stackoverflow.com/a/66752334/3001761

来自 线:

尝试使用函数表达式

export const bar = () => {
return "bar"
}

这应该可以让您监视 bar,即使它被同一模块中的另一个函数使用。