替换测试中的特定模块

Replace specific module in testing

我正在使用 Jest 测试我的 React-Redux 应用程序,作为我的 API 调用的一部分,我正在导入一个获取模块 cross-fetch。我想用 fetch-mock 覆盖或替换它。这是我的文件结构:

Action.js

import fetch from 'cross-fetch';
export const apiCall = () => {
    return fetch('http://url');

Action.test.js

import fetchMock from 'fetch-mock';
import { apiCall } from './Action';
fetchMock.get('*', { hello: 'world' });
describe('actions', () => {
    apiCall().then(
        response => {
            console.log(response)
        })
})

显然此时我还没有设置测试。因为 cross-fetch 的导入更接近它使用它的 fetch 实现的函数,导致它执行实际调用而不是我的模拟。获取被模拟的最佳方法是什么(除了删除 import fetch from 'cross-fetch' 行)?

有没有办法根据调用的节点脚本是 test 还是 build 进行条件导入?或者将模拟提取设置为优先?

fetch-mock 并非旨在替换您正在测试的代码中的 fetch() 调用,您也不需要更改或删除任何导入。相反,它会在您的测试期间提供模拟响应,以便使用 fetch() 发出的请求收到已知的可靠响应。

如果你的项目是webpack项目,那么https://github.com/plasticine/inject-loader就非常有用了。您只需几行代码就可以简单地将任何依赖项与模拟交换。

describe('MyModule', () => {
  let myModule;
  let dependencySpy;

  beforeEach(() => {
    dependencySpy= // {a mock/spy};
    myModule = require('inject-loader!./MyModule')({
      'cross-fetch': {dependencySpy},
    });
  });

  it('should call fetch', () => {
    myModule.function1();
    expect(dependencySpy.calls.length).toBe(1);
  });

});

注意:确保您没有在文件顶部导入被测模块。 require 调用完成了那部分。

有几种方法可以解决这个问题

  1. 你可以stub modules without dependency injection using Sinon

  2. 使用名为 rewire 的库在过程中模拟导入的方法 调用

  3. 重新编写您的原始函数,这样您就不会直接使用导入

    const apiCall = (url, {fetchFn = fetch}) => fetchFn(url);
    
    describe("apiCall", () => {
      it("should call fetch with url", () => {
        const fetchFn = sinon.spy();
        const EXPECTED_URL = "URL";
    
        apiCall(EXPECTED_URL, {fetchFn});
    
        expect(fetchFn.firstCall.args).to.deep.equal([EXPECTED_URL]);
      })
    });
    
  4. 拦截请求并断言响应(这似乎是 fetch-mock 所做的,但我更喜欢 nock,因为文档要好得多)。

    describe("apiCall", () => {
      it("should call fetch with url", () => {
        const EXPECTED_URL = "URL";
        const EXPECTED_RESPONSE = 'domain matched';
    
        var scope = nock(EXPECTED_URL)
        .get('/resource')
        .reply(200, EXPECTED_RESPONSE);
    
        return expect(apiCall(EXPECTED_URL)).to.equal(EXPECTED_RESPONSE);
      })
    });
    

为什么不在你的 Action.js 文件中导出一个 maker 函数,它会注入 fetch 方法,然后 returns 实际的 apiCaller:

Action.js

// this export allows you to setup an apiCaller
// with a different fetcher than in the global scope 
export const makeApiCaller = (fetch) => (url, opts) => fetch(url, opts);
// this export is your default apiCaller that uses cross-fetch
export const apiCall = makeApiCaller(fetch);

然后在你的测试中你可以在某个地方实例化你的 ApiCaller,例如在 before:

Action.test.js

import fetchMock from 'fetch-mock';
import { makeapiCaller } from './Action';

fetchMock.get('*', { hello: 'world' });

// ...

let apiCall; 
before(() {
  apiCall = makeApiCaller(fetch); // injects mocked fetch 
});

describe('actions', () => {
    apiCall('/foo/bar').then(
        response => {
            console.log(response)
        })
})

注意:这样做的好处是,您不必为 apiCall 函数签名引入另一个参数(如另一个答案中所建议的那样),从而保持向后兼容。