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

IT技术 javascript testing mocking jestjs
2021-01-16 07:21:16

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

问题是在导入时间后,foo保留对原始 unmocked 的引用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()}`;
}

但在我看来,这在任何地方都非常难看。

6个回答

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

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

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

很有用。谢谢你。
2021-03-16 07:21:16
这对我来说也是最简单的路线。
2021-03-17 07:21:16
这对我有用,对现有代码的影响最小,并且易于遵循测试。
2021-03-26 07:21:16
如果你使用的是 Node 的 ES module,在 的第一行module.js,记得.js在导入路径中添加,使其变成import * as thisModule from './module.js';
2021-04-01 07:21:16

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

一方面,module.js您导出两个函数(而不是包含这两个函数的对象)。由于module的导出方式,对导出内容的容器的引用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 方法中使用this关键字。

    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容器中的全局引用这不是推荐的方法,因为如果您没有正确地将导出重置为其初始状态,您可能会在其他测试中引入奇怪的行为。

fwiw,我决定的解决方案是使用依赖注入,通过设置默认参数。

所以我会改变

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

这也有导致稍微更好的测试代码的好处:)

更好的测试但糟糕的代码。更改代码并不是一个好主意,因为您找不到测试它的方法。作为一名开发人员,当我查看该代码时,它让我思考了 100 倍为什么module中存在的特定方法作为依赖项传递给同一module中的另一个方法。
2021-03-18 07:21:16
我通常不喜欢依赖注入,因为您允许测试更改代码的编写方式。话虽如此,这比目前的高票答案要好得多,后者非常丑陋
2021-03-31 07:21:16

我遇到了同样的问题,并且由于项目的 linting 标准,exports即使 linting 定义没有阻止,定义类或重写其中的引用也不是代码审查可批准的选项。我偶然发现一个可行的选择是使用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

这应该是可以接受的答案。该插件可以正常工作 && 无需重写测试之外的任何代码。
2021-03-19 07:21:16
谢谢你,如果你在 babel 环境中,那么这就是你正在寻找的答案。
2021-04-11 07:21:16

对我有用:

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);
  });
});
这对我不起作用。funcB 永远不会被调用。
2021-03-30 07:21:16