Testing api call inside useEffect using react-testing-library

29,516

Solution 1

Here is a working unit testing example:

index.tsx:

import React, { useState, FC } from 'react';

export const List: FC<{}> = () => {
  const [data, setData] = useState<number>();
  const getData = (): Promise<any> => {
    return fetch('https://jsonplaceholder.typicode.com/todos/1');
  };

  React.useEffect(() => {
    const func = async () => {
      const data = await getData();
      const value = await data.json();
      setData(value.title);
    };
    func();
  }, []);

  return (
    <div>
      <div data-testid="test">{data}</div>
    </div>
  );
};

index.test.tsx:

import { List } from './';
import React from 'react';
import '@testing-library/jest-dom/extend-expect';
import { render, waitForElement } from '@testing-library/react';

describe('59892259', () => {
  let originFetch;
  beforeEach(() => {
    originFetch = (global as any).fetch;
  });
  afterEach(() => {
    (global as any).fetch = originFetch;
  });
  it('should pass', async () => {
    const fakeResponse = { title: 'example text' };
    const mRes = { json: jest.fn().mockResolvedValueOnce(fakeResponse) };
    const mockedFetch = jest.fn().mockResolvedValueOnce(mRes as any);
    (global as any).fetch = mockedFetch;
    const { getByTestId } = render(<List></List>);
    const div = await waitForElement(() => getByTestId('test'));
    expect(div).toHaveTextContent('example text');
    expect(mockedFetch).toBeCalledTimes(1);
    expect(mRes.json).toBeCalledTimes(1);
  });
});

unit test result:

 PASS  src/stackoverflow/59892259/index.test.tsx (9.816s)
  59892259
    ✓ should pass (63ms)

-----------|----------|----------|----------|----------|-------------------|
File       |  % Stmts | % Branch |  % Funcs |  % Lines | Uncovered Line #s |
-----------|----------|----------|----------|----------|-------------------|
All files  |      100 |      100 |      100 |      100 |                   |
 index.tsx |      100 |      100 |      100 |      100 |                   |
-----------|----------|----------|----------|----------|-------------------|
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        11.73s, estimated 13s

Solution 2

waitForElement is not available from '@testing-library/react' anymore. Docs

Another approach is:

import { act, render } from '@testing-library/react';

it('is a test definition', async () => { // notice the async
  await act(async () => { // this is kind of ugly, but it works.
    render(<TheComponent />
  })

  // this section will run after the effects within TheComponent were triggered
})
Share:
29,516

Related videos on Youtube

hetious
Author by

hetious

Updated on July 09, 2022

Comments

  • hetious
    hetious almost 2 years

    I want to test api call and data returned which should be displayed inside my functional component. I created List component which performs api call. I would like the returned data to be displayed in the component and I use the useState hook for this. Component looks like this:

    const List: FC<{}> = () => {
        const [data, setData] = useState<number>();
        const getData = (): Promise<any> => {
            return fetch('https://jsonplaceholder.typicode.com/todos/1');
        };
    
        React.useEffect(() => {
            const func = async () => {
                const data = await getData();
                const value = await data.json();
                setData(value.title);
            }
            func();
        }, [])
    
        return (
            <div>
                <div id="test">{data}</div>
            </div>
        )
    }
    

    I wrote one test in which I mocked the fetch method. I check if the fetch method has been called and it actually happens. Unfortunately, I don't know how I could test the value returned from response. When I try console.log I just get null and I'd like to get 'example text'. My guess is that I have to wait for this value returned from Promise. Unfortunately, despite trying with methods act and wait, I don't know how to achieve it. Here is my test:

    it('test', async () => {
        let component;
        const fakeResponse = 'example text';
        const mockFetch = Promise.resolve({json: () => Promise.resolve(fakeResponse)});
        const mockedFetch = jest.spyOn(window, 'fetch').mockImplementationOnce(() => mockFetch as any )
        await wait( async () => {
            component = render(<List />);
        })
        const value: Element = component.container.querySelector('#test');
        console.log(value.textContent);
        expect(mockedFetch).toHaveBeenCalledTimes(1);
    })
    
    

    I would be really thankful for any suggestions.

    Second Attempt

    Also tried using data-testid="test" and waitForElement, but still receiving null value.

    updated component deltas:

      const List: FC<{}> = () => {
    -     const [data, setData] = useState<number>();
    +     const [data, setData] = useState<string>('test');
          const getData = (): Promise<any> => {
              return fetch('https://jsonplaceholder.typicode.com/todos/1');
          };
      
          React.useEffect(() => {
              const func = async () => {
                  const data = await getData();
                  const value = await data.json();
                  setData(value.title);
              }
              func();
          }, [])
      
          return (
              <div>
    -             <div id="test">{data}</div>
    +             <div data-testid="test" id="test">{data}</div>
              </div>
          )
      }
    

    and updated test:

    it('test', async () => {
        const fakeResponse = 'example text';
        const mockFetch = Promise.resolve({json: () => Promise.resolve(fakeResponse)});
        const mockedFetch = jest.spyOn(window, 'fetch').mockImplementationOnce(() => mockFetch as any )
        const { getByTestId } = render(<List />);
        expect(getByTestId("test")).toHaveTextContent("test");
        const resolvedValue = await waitForElement(() => getByTestId('test'));
        expect(resolvedValue).toHaveTextContent("example text");
        expect(mockedFetch).toHaveBeenCalledTimes(1);
    })
    
    • hetious
      hetious over 4 years
      Ok, everything was fine in my code and test. Simply I made a mistake in mocked data. In my component I was trying to access title key. In test I mocked a stupid string and that's why I was receiving null value.
    • James B
      James B over 3 years
      found this on a google search -- might I suggest updating your question with the final version. Looks like your post has a few different variations
    • semuzaboi
      semuzaboi over 3 years
      Stumbled upon this, equally interested to know if this assertion passed ` expect(mockedFetch).toHaveBeenCalledTimes(1); `
  • Avin Kavish
    Avin Kavish almost 2 years
    You can use waitFor with an expectation to get the same behaviour. Arguably, that's better than waitForElement because it tests user facing behaviour as opposed to a technical detail. eg: await waitFor(() => expect(getByRole('img')).toBeVisible())
  • Srikanth Gowda
    Srikanth Gowda almost 2 years
    lets say if List component has loading state, if data is fetching loading is true and after fetch loading is false and we set data, so how do we handle this case i don't see above test case is setting mocked data to a particular variable.