hetious
hetious

Reputation: 803

Testing api call inside useEffect using react-testing-library

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

Upvotes: 37

Views: 57415

Answers (3)

yassine-eluharani
yassine-eluharani

Reputation: 135

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

Upvotes: 1

Gabriel Vilches Alves
Gabriel Vilches Alves

Reputation: 412

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

Upvotes: 6

Lin Du
Lin Du

Reputation: 102237

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

Upvotes: 22

Related Questions