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


76

更新:我在https://github.com/magicmark/jest-how-do-i-mock-x/blob/master/src/function-in-same-module/README中收集了此方法和其他方法。 md


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

问题在于,导入时间过后,foo将原始引用保持不变bar

module.js:

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

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

module.test.js:

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

但这是p。我认为到处都是丑陋的:/


更新:我在https://github.com/magicmark/jest-how-do-i-mock-x/blob/master/src/function-in-same-module/README中收集了此方法和其他方法。 md


Answers:


35

问题似乎与您期望解决条的范围有关。

一方面,在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方法中关键字的使用。

    Module.test.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. 就像您说的那样,在全局exports容器中重写全局引用。不建议这样做,因为如果您没有正确地将导出重置为初始状态,则可能会在其他测试中引入奇怪的行为。


35

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

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对循环导入的支持,它的运行非常顺利。


这对我有用,对现有代码的影响最小,并且易于遵循测试。
Shiraz

对我来说,这也是最简单的路线。
Borduhh

很有用。谢谢。
尼克·劳

10

首先,我确定的解决方案是通过设置默认参数来使用依赖项注入

所以我会改变

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

这并不是对组件API的重大更改,通过执行以下操作,我可以轻松地覆盖测试中的bar

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

这样的好处是也会导致更好的测试代码:)


6
我通常不喜欢依赖项注入,因为您允许测试更改代码的编写方式。话虽这么说,这比目前投票率较高的答案要好得多
肖恩(Sean)

10
更好的测试,但代码错误。更改代码不是一个好主意,因为您找不到测试它的方法。作为开发人员,当我查看该代码时,它使我思考100倍的时间,为什么模块中存在的特定方法作为依赖项传递给同一模块中的另一个方法。
Gaurav Kumar

7

我遇到了同样的问题,并且由于项目的掉毛标准,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


3
这应该被接受。该插件可以正常工作&&,无需在测试外重写任何代码。TY-
安德鲁·洛克威尔

谢谢您的帮助,如果您处于通货膨胀的环境中,那么这就是您想要的答案。
莱尔·安德伍德

4

为我工作:

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

1

如果定义导出,则可以将函数作为导出对象的一部分进行引用。然后,您可以分别覆盖模拟中的函数。这是由于导入是如何作为参考而不是副本的。

module.js:

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

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

module.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 is undefiend
theUtherSide
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.