How to test react image component that gets it’s src attribute from a REST api

I have an instance ‘client’ from a JS class like this:

class Client {

  async fetchRandomUser() {
    let response = await fetch("https://randomuser.me/api/?gender=male")
    if (response.status >= 400) {
        throw new Error("Error fetching random user");
    }
    return (await response.json()).results[0];
  }
}

Then I have a React component like this:

import React from 'react';
import client from "./service/Client";

class Person extends React.Component {

    constructor(props) {
        super(props);
        this.state = {image: ''}
    }

    async componentDidMount() {
       const user = await client.fetchRandomUser();
       this.setState({image: user.picture.large});
    }

    setError(error) {
        return this.setState({errorStatus: error});
    }


    render() {
        return (
            <img src={this.state.image} width="128" height="128"/>
        );
    }
}

export default Person;

And a test to go with it:

import React from "react";
import { render, screen, waitFor } from '@testing-library/react';
import Person from './Person';

test('fetches image from random user API', async () => {

  const mockFetchRandomUser = jest.fn().mockImplementation(async () => Promise.resolve({picture:{large: 'personImage'}}));

  jest.mock("./service/Client", () => {
    return {
      fetchRandomUser: mockFetchRandomUser
    };
  });

  const {container} = render(<Person data-testid="person"/>);
  const image = screen.getByRole('img');
  await waitFor(() => expect(image.src !== "").toBeTruthy())
  .then(() => {
    screen.debug(image);
    expect(mockFetchRandomUser).toHaveBeenCalledTimes(1);
  });

});

Inside this test, I am unable to get the waitFor or findByRole or anything for that matter to wait until the tag has gotten it’s src attribute set. How do I do this?

Answer

In the function scope of the test case, use the jest.mock() method will not be hoisted to the top of the code(before any es6 import). That means the imported ./Person component will use the original ./service/Client module when executing the test case. It’s late for mock.

From Using with ES module imports:

If you’re using ES module imports then you’ll normally be inclined to put your import statements at the top of the test file. But often you need to instruct Jest to use a mock before modules use it. For this reason, Jest will automatically hoist jest.mock calls to the top of the module (before any imports).

So you have two choices:

1. Use import() to import the modules dynamically after calling jest.mock() in test case functional scope.

Person.test.jsx:

import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';

test('fetches image from random user API', async () => {
  jest.mock('./service/Client');
  const Person = (await import('./Person')).default;
  const client = (await import('./service/Client')).default;
  client.fetchRandomUser.mockResolvedValueOnce({ picture: { large: 'personImage' } });
  render(<Person data-testid="person" />);
  const image = screen.getByRole('img');
  await waitFor(() => expect(image.src !== '').toBeTruthy());
  expect(client.fetchRandomUser).toHaveBeenCalledTimes(1);
});

2. Use jest.mock() in the module scope of the test file, then you can use static import to import the modules

Person.test.jsx:

import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import Person from './Person';
import client from './service/Client';

jest.mock('./service/Client');

test('fetches image from random user API', async () => {
  client.fetchRandomUser.mockResolvedValueOnce({ picture: { large: 'personImage' } });
  render(<Person data-testid="person" />);
  const image = screen.getByRole('img');
  await waitFor(() => expect(image.src !== '').toBeTruthy());
  expect(client.fetchRandomUser).toHaveBeenCalledTimes(1);
});

jestjs version: "jest": "^26.6.3"