Testing api call inside useEffect using react-testing-library
Asked Answered
B

3

37

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);
})
Bubonocele answered 24/1, 2020 at 7:41 Comment(3)
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.Bubonocele
found this on a google search -- might I suggest updating your question with the final version. Looks like your post has a few different variationsKira
Stumbled upon this, equally interested to know if this assertion passed ` expect(mockedFetch).toHaveBeenCalledTimes(1); `Paraph
D
22

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
Desiccator answered 3/11, 2020 at 5:45 Comment(1)
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.Lanner
J
6

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
})
Jung answered 8/3, 2022 at 3:39 Comment(1)
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())Gerfen
T
1

What worked for me was a combination of both answers

it("should fetch data", async ()=>{
    const fakeResponse = {title : "Test"}
    const mRes = { json: jest.fn().mockResolvedValueOnce(fakeResponse) };
    const mockedFetch = jest.fn().mockResolvedValueOnce(mRes);
    global.fetch = mockedFetch;
    render(<Component/>);
    await act(async ()=>{
        await waitFor(() => expect(mockedFetch).toHaveBeenCalledTimes(1))
    })
})

NB: the code above is in javascript, but I don't think there is much difference between js and ts

Tadtada answered 6/6, 2022 at 11:23 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.