【问题标题】:How to mock imported named function in Jest when module is unmocked未模拟模块时如何在 Jest 中模拟导入的命名函数
【发布时间】:2017-02-06 21:39:07
【问题描述】:

我尝试在 Jest 中测试以下模块:

// myModule.js

export function otherFn() {
  console.log('do something');
}

export function testFn() {
  otherFn();

  // do other things
}

如上所示,它导出了一些命名函数,重要的是testFn 使用了otherFn。

在我为testFn 编写单元测试时开玩笑,我想模拟otherFn 函数,因为我不希望otherFn 中的错误影响我对testFn 的单元测试。我的问题是我不确定最好的方法:

// myModule.test.js
jest.unmock('myModule');

import { testFn, otherFn } from 'myModule';

describe('test category', () => {
  it('tests something about testFn', () => {
    // I want to mock "otherFn" here but can't reassign
    // a.k.a. can't do otherFn = jest.fn()
  });
});

感谢任何帮助/见解。

【问题讨论】:

  • 我不会这样做。无论如何,嘲笑通常不是您想做的事情。如果您需要模拟某些东西(由于进行服务器调用/等),那么您应该将 otherFn 提取到一个单独的模块中并模拟它。
  • @kentcdodds,您能否澄清一下“Mocking 通常不是您想要做的事情”的意思?这似乎是一个相当宽泛(过于宽泛?)的陈述,因为嘲笑肯定是经常使用的东西,大概是出于(至少某些)充分的理由。那么,您可能是指为什么在 此处 模拟可能不好,还是您的意思是一般意义上的?
  • 通常模拟是测试实现细节。尤其是在这个级别上,它导致的测试并没有真正验证您的测试是否有效(而不是您的代码有效)这一事实。
  • 我正准备称第一个评论者为白痴,然后才意识到他是谁。不过,这条评论让我很困惑。首先,模拟对于测试现代应用程序至关重要。另外,我也有同样的情况,把函数分成单独的模块是没有意义的。
  • 为了记录,自从几年前写这个问题以来,我已经改变了我想要做多少嘲笑(并且不再像这样嘲笑)的态度。这些天来,我非常同意@kentcdodds 和他的测试理念(并强烈推荐他的博客和@testing-library/react 用于任何Reacters),但我知道这是一个有争议的话题。

标签: unit-testing ecmascript-6 jestjs


【解决方案1】:

在jest.mock() 中使用jest.requireActual()

jest.requireActual(moduleName)

返回实际模块而不是模拟,绕过对模块是否应该接收模拟实现的所有检查。

示例

我更喜欢这种简洁的用法,你需要并在返回的对象中传播:

// myModule.test.js

import { otherFn } from './myModule.js'

jest.mock('./myModule.js', () => ({
  ...(jest.requireActual('./myModule.js')),
  otherFn: jest.fn()
}))

describe('test category', () => {
  it('tests something about otherFn', () => {
    otherFn.mockReturnValue('foo')
    expect(otherFn()).toBe('foo')
  })
})

在 Jest 的 Manual Mocks 文档中也引用了此方法(接近 Examples 的末尾):

为确保手动模拟与其实际实现保持同步,在手动模拟中要求使用jest.requireActual(moduleName) 的真实模块并在导出之前使用模拟函数对其进行修改可能会很有用。

【讨论】:

  • Fwiw,您可以通过删除 return 语句并将箭头函数体包装在括号中来使其更加简洁:例如。 jest.mock('./myModule', () => ({ ...jest.requireActual('./myModule'), otherFn: () => {}}))
  • 这很棒!这应该是公认的答案。
  • 在这种情况下,您将如何测试 otherFun 是否被调用?假设otherFn: jest.fn()
  • 这个答案实际上并没有回答问题。提供的解决方案不调用testFn 反过来应该调用otherFn 的模拟。如果您致电testFn,它仍将使用原始otherFn。至少,它在 TypeScript 中对我不起作用。请参阅explaination。
【解决方案2】:

看来我参加这个聚会迟到了,但是是的,这是可能的。

testFn 只需调用otherFn 使用模块。

如果testFn 使用模块调用otherFn,则可以模拟otherFn 的模块导出,testFn 将调用模拟。


这是一个工作示例:

myModule.js

import * as myModule from './myModule';  // import myModule into itself

export function otherFn() {
  return 'original value';
}

export function testFn() {
  const result = myModule.otherFn();  // call otherFn using the module

  // do other things

  return result;
}

myModule.test.js

import * as myModule from './myModule';

describe('test category', () => {
  it('tests something about testFn', () => {
    const mock = jest.spyOn(myModule, 'otherFn');  // spy on otherFn
    mock.mockReturnValue('mocked value');  // mock the return value

    expect(myModule.testFn()).toBe('mocked value');  // SUCCESS

    mock.mockRestore();  // restore otherFn
  });
});

【讨论】:

  • 这本质上是 Facebook 使用的方法的 ES6 版本,由 Facebook 开发人员在this post 中间进行了描述。
  • 而不是将 myModule 导入自身,只需调用 exports.otherFn()
  • @andrhamm exports 在 ES6 中不存在。现在调用 exports.otherFn() 可以工作,因为 ES6 正在被编译为更早的模块语法,但是当 ES6 被原生支持时它会中断。
  • 现在有这个确切的问题,我确定我以前遇到过这个问题。我不得不删除大量的 export。 以帮助 tree shaking 并破坏了许多测试。我会看看这是否有任何影响,但它似乎很hacky。我已经多次遇到这个问题,就像其他答案所说的那样,像 babel-plugin-rewire 甚至更好的东西,npmjs.com/package/rewiremock 我很确定也可以做到以上。
  • 是否可以模拟一个抛出而不是模拟一个返回值?编辑:你可以,这是stackoverflow.com/a/50656680/2548010
【解决方案3】:
import m from '../myModule';

对我不起作用,我确实使用过:

import * as m from '../myModule';

m.otherFn = jest.fn();

【讨论】:

  • 测试后如何恢复otherFn的原有功能,不干扰其他测试?
  • 我认为您可以配置 jest 以在每次测试后清除模拟?来自文档:“clearMocks 配置选项可用于在测试之间自动清除模拟。”。您可以设置clearkMocks: true jest package.json 配置。 facebook.github.io/jest/docs/en/mock-function-api.html
  • 如果这是改变全局状态的问题,您总是可以将原始功能存储在某种测试变量中并在测试后将其带回
  • 常量原始; beforeAll(() = > { original = m.otherFn; m.otherFn = jest.fn(); }) afterAll(() => { m.otherFn = original; }) 它应该可以工作,但是我没有测试它
【解决方案4】:

我知道很久以前有人问过这个问题,但我只是遇到了这种情况,终于找到了一个可行的解决方案。所以我想我会在这里分享。

对于模块:

// myModule.js

export function otherFn() {
  console.log('do something');
}

export function testFn() {
  otherFn();

  // do other things
}

您可以更改为以下内容:

// myModule.js

export const otherFn = () => {
  console.log('do something');
}

export const testFn = () => {
  otherFn();

  // do other things
}

将它们导出为常量而不是函数。我相信这个问题与 JavaScript 中的提升有关,使用 const 可以防止这种行为。

然后在你的测试中你可以有如下的东西:

import * as myModule from 'myModule';


describe('...', () => {
  jest.spyOn(myModule, 'otherFn').mockReturnValue('what ever you want to return');

  // or

  myModule.otherFn = jest.fn(() => {
    // your mock implementation
  });
});

您的模拟现在应该可以正常工作了。

【讨论】:

  • 我现在遇到了同样的问题,我想知道如果 otherFn 不是一个函数,而是一个数组,你将如何模拟它?所以testFn 正在读取一个也从模块中导出的数组,我想在我的测试中模拟该数组以改变testFn 的行为而不模拟自身。
【解决方案5】:

转译后的代码将不允许 babel 检索 otherFn() 所指的绑定。如果你使用函数表达式,你应该可以实现模拟otherFn()。

// myModule.js
exports.otherFn = () => {
  console.log('do something');
}

exports.testFn = () => {
  exports.otherFn();

  // do other things
}

 

// myModule.test.js
import m from '../myModule';

m.otherFn = jest.fn();

但是正如@kentcdodds 在之前的评论中提到的,你可能不想模拟otherFn()。相反,只需为 otherFn() 编写一个新规范并模拟它正在进行的任何必要调用。

例如,如果otherFn() 正在发出一个 http 请求...

// myModule.js
exports.otherFn = () => {
  http.get('http://some-api.com', (res) => {
    // handle stuff
  });
};

在这里,您需要模拟 http.get 并根据您模拟的实现更新您的断言。

// myModule.test.js
jest.mock('http', () => ({
  get: jest.fn(() => {
    console.log('test');
  }),
}));

【讨论】:

  • 如果其他几个模块使用 otherFn 和 testFn 怎么办?您是否需要在使用这两个模块(无论堆栈有多深)的所有测试文件中设置 http 模拟?另外,如果您已经对 testFn 进行了测试,为什么不在使用 testFn 的模块中直接存根 testFn 而不是 http?
  • 因此,如果otherFn 被破坏,所有依赖于该测试的测试都将失败。此外,如果 otherFn 内部有 5 个 if,您可能需要测试您的 testFn 是否适用于所有这些子案例。您现在将有更多的代码路径可供测试。
【解决方案6】:

基于Brian Adams' answer,这就是我能够在 TypeScript 中使用相同方法的方式。此外,使用jest.doMock() 可以仅在测试文件的某些特定测试中模拟模块功能,并为每个模块提供单独的模拟实现。

src/module.ts

import * as module from './module';

function foo(): string {
  return `foo${module.bar()}`;
}

function bar(): string {
  return 'bar';
}

export { foo, bar };

test/module.test.ts

import { mockModulePartially } from './helpers';

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

const { foo } = module;

describe('test suite', () => {
  beforeEach(function() {
    jest.resetModules();
  });

  it('do not mock bar 1', async() => {
    expect(foo()).toEqual('foobar');
  });

  it('mock bar', async() => {
    mockModulePartially('../src/module', () => ({
      bar: jest.fn().mockImplementation(() => 'BAR')
    }));
    const module = await import('../src/module');
    const { foo } = module;
    expect(foo()).toEqual('fooBAR');
  });

  it('do not mock bar 2', async() => {
    expect(foo()).toEqual('foobar');
  });
});

test/helpers.ts

export function mockModulePartially(
  modulePath: string,
  mocksCreator: (originalModule: any) => Record<string, any>
): void {
  const testRelativePath = path.relative(path.dirname(expect.getState().testPath), __dirname);
  const fixedModulePath = path.relative(testRelativePath, modulePath);
  jest.doMock(fixedModulePath, () => {
    const originalModule = jest.requireActual(fixedModulePath);
    return { ...originalModule, ...mocksCreator(originalModule) };
  });
}

模块的模拟函数被移动到位于单独文件中的辅助函数mockModulePartially,因此可以从不同的测试文件中使用它(通常可以位于其他目录中)。它依赖expect.getState().testPath 来修复被模拟的模块(modulePath)的路径(使其相对于包含mockModulePartially 的helpers.ts)。 mocksCreator 函数作为第二个参数传递给 mockModulePartially 应该返回模块的模拟。此函数接收originalModule,并且模拟实现可以选择依赖它。

【讨论】:

    【解决方案7】:

    我通过在这里找到的各种答案解决了我的问题:

    myModule.js

    import * as myModule from './myModule';  // import myModule into itself
    
    export function otherFn() {
      return 'original value';
    }
    
    export function testFn() {
      const result = myModule.otherFn();  // call otherFn using the module
    
      // do other things
    
      return result;
    }
    

    myModule.test.js

    import * as myModule from './myModule';
    
    describe('test category', () => {
      let otherFnOrig;
    
      beforeAll(() => {
        otherFnOrig = myModule.otherFn;
        myModule.otherFn = jest.fn();
      });
    
      afterAll(() => {
        myModule.otherFn = otherFnOrig;
      });
    
      it('tests something about testFn', () => {
        // using mock to make the tests
      });
    });
    

    【讨论】:

      【解决方案8】:

      除了这里的第一个答案,您还可以使用babel-plugin-rewire 来模拟导入的命名函数。您可以从表面上查看该部分 named function rewiring。

      对您的情况而言,其中一个直接好处是您无需更改从函数中调用其他函数的方式。

      【讨论】:

      • 如何配置 babel-plugin-rewire 以使用 node.js?
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-03-27
      • 1970-01-01
      • 2021-01-21
      • 1970-01-01
      相关资源
      最近更新 更多