jefflau / jest-fetch-mock

Jest mock for fetch
MIT License
886 stars 117 forks source link

fetch.resetMocks(); makes the fetch call to return undefined. #81

Open twoneks opened 6 years ago

twoneks commented 6 years ago

Hi! I have a problem using the fetch.resetMocks(); in the before each statement.

// app/utils/tests/request.test.js

import request from '../request';

describe('request', () => {
  beforeEach(() => {
    fetch.resetMocks();
  });
  it('perform the fetch with the default headers', () => {
    const defaultHeaders = {
      'Content-Type': 'application/json',
      'Access-Control-Allow-Origin': '*',
      'Access-Control-Allow-Headers': 'Content-Type',
    }
    request('http://fake_url', {})

    expect(fetch.mock.calls.length).toBe(1);
    expect(fetch.mock.calls[0][1].headers).toEqual(defaultHeaders);
  });

// app/utils/request.js
....
export default function request(
  url,
  extraHeaders = {},
  method = 'GET',
  body = undefined,
) {
  const headers = Object.assign(
    { headers: Object.assign(CORSHeaders.headers, extraHeaders) },
    { method },
    { body: JSON.stringify(body) },
  );

  return fetch(url, headers)
    .then(checkStatus)
    .then(parseJSON);
}

Executing the test with or without fetch.resetMocks(); affects the response of the test.

Without it works well. With I get

 FAIL  app/utils/tests/request.test.js
  ● request › perform the fetch with the default headers

    TypeError: Cannot read property 'then' of undefined

      60 |   );
      61 | 
    > 62 |   return fetch(url, headers)
         |          ^
      63 |     .then(checkStatus)
      64 |     .then(parseJSON);
      65 | }

      at request (app/utils/request.js:62:10)
      at Object.<anonymous> (app/utils/tests/request.test.js:17:5)

I want to execute more than one test over fetch in this file so I really need to reset it. Great project by the way. Cheers.

jefflau commented 6 years ago

Can't immediately see what's going, but it doesn't look like you have a mock inside your test file:

describe('request', () => {
 beforeEach(() => {
   fetch.resetMocks();
 });
 it('perform the fetch with the default headers', () => {
   const defaultHeaders = {
     'Content-Type': 'application/json',
     'Access-Control-Allow-Origin': '*',
     'Access-Control-Allow-Headers': 'Content-Type',
   }
   //Shouldn't there be a mock here if you're resetting it every time?
   fetch.once(/* args */ )
   request('http://fake_url', {})

   expect(fetch.mock.calls.length).toBe(1);
   expect(fetch.mock.calls[0][1].headers).toEqual(defaultHeaders);
 });
twoneks commented 6 years ago

Actually in this particular test I don't want to mock a response but just check the header and that the fetch call has been performed. I share the entire file, maybe it will make more sense.

/**
 * Test request
 */

import request from '../request';

describe('request', () => {
  // beforeEach(() => {
  //   fetch.resetMocks()
  // });
  it('perform the fetch with the default param', () => {
    const defaultHeaders = {
      'Content-Type': 'application/json',
      'Access-Control-Allow-Origin': '*',
      'Access-Control-Allow-Headers': 'Content-Type',
    };
    request('http://fake_url', {});

    expect(fetch.mock.calls.length).toBe(1);
    expect(fetch.mock.calls[0][1].headers).toEqual(defaultHeaders);
  });

  describe('allow to customize headers', () => {
    it('merge new headers without deleting default values', () => {
      const customHeaders = {
        CustomParam: 'CustomValue',
      };
      request('http://fake_url', customHeaders);

      expect(fetch.mock.calls[1][1].headers).toEqual({
        'Content-Type': 'application/json',
        'Access-Control-Allow-Origin': '*',
        'Access-Control-Allow-Headers': 'Content-Type',
        CustomParam: 'CustomValue',
      });
    });

    it('correctly override the defautl values', () => {
      const customHeaders = {
        CustomParam: 'CustomValue',
        'Access-Control-Allow-Origin': '0.0.0.0',
      };
      request('http://fake_url', customHeaders);

      expect(fetch.mock.calls[2][1].headers).toEqual({
        'Content-Type': 'application/json',
        'Access-Control-Allow-Origin': '0.0.0.0',
        'Access-Control-Allow-Headers': 'Content-Type',
        CustomParam: 'CustomValue',
      });
    });
  });

  describe('allow to customize method and body', () => {
    it('merge new headers without deleting default values', () => {
      request('http://fake_url', {}, 'POST', { id: '1234567890' });
      expect(fetch.mock.calls[3][1].method).toEqual('POST');
      expect(fetch.mock.calls[3][1].body).toEqual('{"id":"1234567890"}');
    });
  });

  it('return a parsed JSON', () => {
    fetch.mockResponseOnce(JSON.stringify({ data: '12345' }));
    request('http://fake_url', {}).then(res => {
      expect(res.data).toEqual('12345');
    });
  });

  it('return null on code from 204 to 205', () => {
    fetch.mockResponseOnce(JSON.stringify({}), { status: 204 });
    request('http://fake_url', {}).then(res => {
      expect(res.data).toEqual(null);
    });
  });

  it('handle erorrs', () => {
    let responseError;
    const expectedError = new Error('Internal Server Error');
    fetch.mockResponseOnce(JSON.stringify({}), {
      status: 500,
    });

    Promise.all([
      request('http://fake_url', {}).catch(err => {
        responseError = err;
        expect(responseError).toEqual(expectedError);
      }),
    ]);
  });
});

The first 4 tests are meant to test the parameters. I don't care about the response. As you can see in each test I have to increment the index in the fetch.mock.calls[0][1] and this is creating a dependency between one test and another. If they are not executed in this sequence they will fail. Moreover I would like to add in each test the assertion expect(fetch.mock.calls.length).toBe(1); but doing so it will fail because in the second test will be expect(fetch.mock.calls.length).toBe(2); ans so on creating a dependency between test execution. I thought the beforeEach(() => {fetch.resetMocks();}); is meant to be used to reset the calls made over fetch. Am I wrong?

csvwolf commented 5 years ago

The same problem with you! Here is my code:

  get(url, data = {}) {
    const path = new URL(url)
    path.search = new URLSearchParams(data)
    return fetch(path).then(resp =>
      new Promise((resolve) =>
        resp.json()
          .then(data => resolve(data))
          .catch(() => resolve(resp.text()))
      )
    )

jest.setup.js

require('jest-localstorage-mock')
const { JSDOM } = require('jsdom')
const dom = new JSDOM(
  '<!DOCTYPE html><html><body></body></html>',
  {
    runScripts: 'dangerously'
  })
global.window = dom.window
global.document = dom.window.document
global.fetch = require('jest-fetch-mock')

fetch is inside my test case and throw an error:

TypeError: Cannot read property 'then' of undefined

      3 |     const path = new URL(url)
      4 |     path.search = new URLSearchParams(data)
    > 5 |     return global.fetch(path).then(resp =>
        |            ^
      6 |       new Promise((resolve) =>
      7 |         resp.json()
      8 |           .then(data => resolve(data))

But it will be successful with the following:

  get(url, data = {}) {
    const path = new URL(url)
    path.search = new URLSearchParams(data)
    return window.fetch(path).then(resp => resp.text()).then(text => {
      let result
      try {
        result = JSON.parse(text)
      } catch (e) {
        result = text
      }
      return result
    })
  }

update:

When I use this in my code, it works:

  global.fetch.mockResponse([
    JSON.stringify({ 'perf': 'data' })
  ], [
    '@@ -0,0 +1,14 @@\n+console.log(1);'
  ])

But two global.fetch.once('@@ -0,0 +1,14 @@\n+console.log(1);') Will Be Error

slavafomin commented 5 years ago

I'm having exactly the same problem. After running fetch.resetMocks() the fetch() call returns undefined instead of a promise.

What could be wrong?

nikolai-katkov commented 5 years ago

@slavafomin fetch.resetMocks() calls jest's mockReset function, which leads to those undefined values. It's a feature, not a bug 😎 Regarding original problem, I managed to solve it by the following change:

// jest.config.js
module.exports = {
    ...
-    resetMocks: true,
+    clearMocks: true,
    ...
};
a7madgamal commented 5 years ago

any updates or workarounds?

jihchi commented 5 years ago

It seems like mocked default implementation has been cleaned up due to Jest's mockReset.

I've managed similar issue by adding following workaround:

beforeEach(() => {
  fetch.resetMocks();
  fetch.mockResponse('');
 });
Avladd commented 5 years ago

TLDR: Async code was causing the wrong test to throw the error. Check the solution to see what I mean.

Original: None of these suggestions worked for me. And the problem is it's inconsistent. Using Create-react-app and React Testing Library. My code looks something like this:

//test.js

global.fetch = require('jest-fetch-mock')

afterEach( ()=> {
fetch.resetMocks()
//clear other mocks
}

test(<MyComponent/>, () => {
const data = {};
fetch.once(JSON.stringify(data));

render(<MyComponent />)
} )

and the actual code on the page

componentDidMount(){
fetch('/data')
 .then(res => res.json())
 .then(data => useData(data));
}

I have 14 other tests that rely on the exact same code in this file, loading the same component, executing the same fetch which returns the same data and they all work properly. But this last test just keeps throwing the FetchError: invalid json response body.

The same function on the page is calling the fetch, and handling it the exact same way as in all the other tests - it's identical. I even checked the stringified responses returned from the mocked fetch and they seem identical between when it works and when it doesn't;

Solution:

I found the issue in my particular file. In the test above the failing one, which was seemingly passing I had

fetch.resetMocks():
fetch.once(JSON.stringify()) // undefined

Replacing that with a JSON.stringify({ }) solved it. Why? No idea. Because it's async code. I never needed to check the actual response in the test above the failing one, so I wasn't waiting for it. But it was erroring out, only doing so in async manner, leading Jest and me to think the next test was the one throwing errors.

Waiting on the response

fetch.resetMocks():
fetch.once(JSON.stringify()) // undefined
await wait();

resulted in the error being thrown/caught in the correct test which was until now seemingly passing. And using JSON.stringify({}) worked because it removed the actual error.

yzaroui commented 5 years ago

@slavafomin fetch.resetMocks() calls jest's mockReset function, which leads to those undefined values. It's a feature, not a bug 😎

How to get around that "feature"?

const res = await fetch(url, options);

  if (!res.ok) {  // here res is undefined

Anyone found a solution for this?