Replace specific module in testing

2019-06-15 18:03发布

问题:

I am testing my React-Redux app with Jest and as part of this in my API calls I am importing a fetch module cross-fetch. I want to override or replace this with fetch-mock. Here is my file structure:

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

Obviously at this point I haven't set up the test. Because cross-fetch is imported closer to the function it uses it's implementation of fetch, causing it to do the actual call instead of my mock. Whats the best way of getting the fetch to be mocked (apart from removing the import fetch from 'cross-fetch' line)?

Is there a way to do a conditional import depending on whether the node script called is test or build? Or set the mocked fetch to take priority?

回答1:

If your project is a webpack project, then https://github.com/plasticine/inject-loader is very useful. You can simply swap any dependency with a mock in just a few lines of code.

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

});

Note: make sure you don't import the module under test at the top of your file. the require call does that part.



回答2:

fetch-mock is not intended to replace the fetch() calls in the code you are testing nor do you need to change or remove any imports. Instead, it provides mock responses during your tests so that requests made with fetch() receive known, reliable responses.



回答3:

There are a few ways you can approach this problem

  1. You can stub modules without dependency injection using Sinon.

  2. Use a lib called rewire to mock imported methods in procedural calls

  3. Re write your original function so that you are not using the import directly

    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. Intercept the request and assert on the response (this is appears to be what fetch-mock does however I prefer nock as the documentation is much better).

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


回答4:

Why not also export a maker function in your Action.js file that will get the fetch method injected and then returns the actual 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);

Then in your tests you can somewhere instantiate your ApiCaller, e.g. in the 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)
        })
})

Note: the benefit of doing it this way, is that you don't have to introduce another argument to your apiCall function signature (as proposed in another answer) and thus stay backwards compatible.