Search code examples
reactjsreact-hooksaxiosjestjsreact-testing-library

How to test a react app with an axios api call?


I'm trying to test a component that renders an image that's fetched from the unpsplash api using axios. However the test keeps failing but the app is working.

The error the test throws is: Expected the element to have attribute: src="https://via.placeholder.com/300x300.png?text=robin" Received: src="https://via.placeholder.com/200x300/86efac?text=Loading..."

I think this must mean I'm not mocking the axios request properly or is it something to do with lazy loading?

My test:

import { screen, waitFor } from "@testing-library/react";
import { renderWithProviders } from "../utils/utils-for-tests";
import axios from "axios";
import BirdImg from "../components/BirdImg";

class IntersectionObserver {
  observe() {
    return null;
  }

  disconnect() {
    return null;
  }
}

window.IntersectionObserver = IntersectionObserver;

jest.mock("axios", () => ({
  get: jest.fn(),
}));

const mockLoadedResponse = {
  data: {
    results: [
      {
        urls: {
          thumb: "https://via.placeholder.com/300x300.png?text=robin",
        },
      },
    ],
  },
};
test("shows the image", async () => {
  axios.get.mockResolvedValue(mockLoadedResponse);

  renderWithProviders(<BirdImg name="robin" />);
  const birdImage = await screen.findByRole("img");

  await waitFor(() => {
    expect(birdImage).toBeInTheDocument();
    expect(birdImage).toHaveAttribute(
      "src",
      "https://via.placeholder.com/300x300.png?text=robin"
    );
  });
});

The component I want to test:

import { useEffect, useRef, useState } from "react";
import { fetchBirdImg } from "../api/unsplash";

function BirdImg({ name }) {
  const imgRef = useRef(null);
  const [loaded, setLoaded] = useState(false);
  const [error, setError] = useState(false);

  useEffect(() => {
    const observer = new IntersectionObserver((entries) => {
      entries.forEach((entry) => {
        if (entry.isIntersecting) {
          fetchBirdImg(name).then((data) => {
            imgRef.current.src = data[0].urls.thumb;
            setLoaded(true);
          });

          observer.unobserve(imgRef.current);
        }
      });
    });

    observer.observe(imgRef.current);

    return () => observer.disconnect();
  }, [name]);

  let imgSrc;

  if (loaded === false) {
    imgSrc = `https://via.placeholder.com/200x300/86efac?text=Loading...`;
  }
  if (loaded) {
    imgSrc = imgRef.current.src;
  }
  if (error === true) {
    imgSrc = `https://via.placeholder.com/200x300/86efac?text=Error`;
  }

  return (
    <img
      ref={imgRef}
      onLoad={() => {
        setLoaded(true);
      }}
      onError={() => {
        setError(true);
      }}
      src={imgSrc}
      alt={name}
      className="w-20"
    />
  );
}

export default BirdImg;

The api call:

import axios from "axios";

async function fetchBirdImg(name) {
  const response = await axios.get(
    "https://api.unsplash.com/search/photos?per_page=1&orientation=portrait",

    {
      headers: {
        Authorization: "auth key",
      },
      params: { query: `${name}` },
    }
  );
  return response.data.results;
}

export { fetchBirdImg };

Solution

  • You should also mock IntersectionObserver() constructor and unobserve() method

    You can simply mock the fetchBirdImg API function instead of the axios.

    E.g.

    Directory structure:

    ⚡  tree -L 2 -I 'node_modules'
    .
    ├── BirdImg.jsx
    ├── BirdImg.test.jsx
    └── api.js
    

    BirdImg.test.jsx:

    import React from 'react';
    import { screen, waitFor, render } from '@testing-library/react';
    import '@testing-library/jest-dom';
    import BirdImg from './BirdImg';
    import { fetchBirdImg } from './api';
    
    jest.mock('./api');
    
    const mockLoadedResponse = [{ urls: { thumb: 'https://via.placeholder.com/300x300.png?text=robin' } }];
    test('shows the image', async () => {
      let _callback;
      const intersectionObserverEntries = [{ isIntersecting: true }];
      class IntersectionObserver {
        constructor(callback) {
          _callback = callback;
        }
        observe() {
          return null;
        }
        unobserve() {
          return null;
        }
        disconnect() {
          return null;
        }
      }
    
      fetchBirdImg.mockResolvedValueOnce(mockLoadedResponse);
      window.IntersectionObserver = IntersectionObserver;
    
      render(<BirdImg name="robin" />);
      const birdImage = screen.getByRole('img');
      expect(birdImage).toHaveAttribute('src', 'https://via.placeholder.com/200x300/86efac?text=Loading...');
      _callback(intersectionObserverEntries);
      await waitFor(() => {
        expect(birdImage).toBeInTheDocument();
        expect(birdImage).toHaveAttribute('src', 'https://via.placeholder.com/300x300.png?text=robin');
      });
    });
    

    We declared a _callback variable to hold the real callback and call it manually with mocked intersectionObserverEntries object after the instantiation of the IntersectionObserver class.

    Test result:

     PASS  stackoverflow/76189869/BirdImg.test.jsx (20.943 s)
      ✓ shows the image (89 ms)
    
    -------------|---------|----------|---------|---------|-------------------
    File         | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
    -------------|---------|----------|---------|---------|-------------------
    All files    |   83.87 |       75 |   66.67 |   86.21 |                   
     BirdImg.jsx |   88.89 |       75 |      75 |      92 | 34,38             
     api.js      |      50 |      100 |       0 |      50 | 4-13              
    -------------|---------|----------|---------|---------|-------------------
    Test Suites: 1 passed, 1 total
    Tests:       1 passed, 1 total
    Snapshots:   0 total
    Time:        22.404 s
    Ran all test suites related to changed files.