如何使用jest在同一模块中模拟函数

时间:2017-07-14 20:33:23

标签: javascript testing mocking jestjs

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

问题是,在导入时间后,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。在我看来,到处都是丑陋的:/

6 个答案:

答案 0 :(得分:15)

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

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

答案 1 :(得分:13)

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

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

答案 2 :(得分:5)

fwiw,我解决的解决方案是使用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()}`;
}

这不是对我的组件的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');
    });
});

这样做的好处是可以提供更好的测试代码:)

答案 3 :(得分:1)

我也遇到了同样的问题,由于项目的掉毛标准,即使没有掉毛定义,在exports中定义类或重写引用也不是代码审查批准的选项。我偶然发现一个可行的选择是使用babel-rewire-plugin,它至少在外观上更清洁。当我发现它在我可以访问的另一个项目中使用时,我注意到它已经在我链接了here的类似问题的答案中。这是针对链接的问题提供的针对此问题(且无需使用间谍)的摘要,以供参考(由于我不是异教徒,因此我还添加了分号,除了删除了间谍之外):

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

答案 4 :(得分:0)

为我工作:

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

答案 5 :(得分:0)

如果您使用 Babel(即 @babel/parser)来处理代码的转译,babel-plugin-explicit-exports-references1 npm 包通过使“丑陋”{ {1}} 个替换在编译时透明。有关详细信息,请参阅 the original problem thread


1 注意:我写了这个插件!