'Trigger useEffect in Jest and Enzyme testing

I'm using Jest and Enzyme to test a React functional component.

MyComponent:

export const getGroups = async () => {
    const data = await fetch(groupApiUrl);
    return await data.json()
};

export default function MyWidget({
  groupId,
}) {
  // Store group object in state
  const [group, setGroup] = useState(null);

  // Retrive groups on load
  useEffect(() => {
    if (groupId && group === null) {
      const runEffect = async () => {
        const  { groups  } = await getGroups();
        const groupData = groups.find(
          g => g.name === groupId || g.id === Number(groupId)
        );

        setGroup(groupData);
      };
      runEffect();
    }
  }, [group, groupId]);

  const params =
    group && `&id=${group.id}&name=${group.name}`;
  const src = `https://mylink.com?${params ? params : ''}`;

  return (
    <iframe src={src}></iframe>
  );
}

When I write this test:

  it('handles groupId and api call ', () => {
    // the effect will get called
    // the effect will call getGroups
    // the iframe will contain group parameters for the given groupId


   act(()=> {
        const wrapper = shallow(<MyWidget surface={`${USAGE_SURFACES.metrics}`} groupId={1} />) 
        console.log(wrapper.find("iframe").prop('src'))
    })
   })

The returned src doesn't contain the group information in the url. How do I trigger useEffect and and everything inside that?

EDIT: One thing I learned is the shallow will not trigger useEffect. I'm still not getting the correct src but I've switched to mount instead of shallow



Solution 1:[1]

Here's a minimal, complete example of mocking fetch. Your component pretty much boils down to the generic fire-fetch-and-set-state-with-response-data idiom:

import React, {useEffect, useState} from "react";

export default function Users() {
  const [users, setUsers] = useState([]);

  useEffect(() => {
    (async () => {
      const res = await fetch("https://jsonplaceholder.typicode.com/users");
      setUsers(await res.json());
    })();
  }, []);

  return <p>there are {users.length} users</p>;
};

Feel free to run this component in the browser:

<script type="text/babel" defer>
const {useState, useEffect} = React;

const Users = () => {
  const [users, setUsers] = useState([]);

  useEffect(() => {
    (async () => {
      const res = await fetch("https://jsonplaceholder.typicode.com/users");
      setUsers(await res.json());
    })();
  }, []);

  return <p>there are {users.length} users</p>;
};

ReactDOM.render(<Users />, document.body);
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.26.0/babel.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>

You can see the component initially renders a value of 0, then when the request arrives, all 10 user objects are in state and a second render is triggered showing the updated text.

Let's write a naive (but incorrect) unit test, mocking fetch:

import {act} from "react-dom/test-utils";
import React from "react";
import Enzyme, {mount} from "enzyme";
import Adapter from "enzyme-adapter-react-16";

import Users from "../src/Users";

Enzyme.configure({adapter: new Adapter()});

describe("Users", () => {
  let wrapper;
  let users;
  
  beforeEach(() => {
    const mockResponseData = [{id: 1}, {id: 2}, {id: 3}];
    users = mockResponseData.map(e => ({...e}));
    jest.clearAllMocks();
    global.fetch = jest.fn(async () => ({
      json: async () => mockResponseData
    }));
    wrapper = mount(<Users />);
  });
  
  it("renders a count of users", () => {
    const p = wrapper.find("p");
    expect(p.exists()).toBe(true);
    expect(p.text()).toEqual("there are 3 users");
  });
});

All seems well--we load up the wrapper, find the paragraph and check the text. But running it gives:

Error: expect(received).toEqual(expected) // deep equality

Expected: "there are 3 users"
Received: "there are 0 users"

Clearly, the promise isn't being awaited and the wrapper is not registering the change. The assertions run synchronously on the call stack as the promise waits in the task queue. By the time the promise resolves with the data, the suite has ended.

We want to get the test block to await the next tick, that is, wait for the call stack and pending promises to resolve before running. Node provides setImmediate or process.nextTick for achieving this.

Finally, the wrapper.update() function enables synchronization with the React component tree so we can see the updated DOM.

Here's the final working test:

import {act} from "react-dom/test-utils";
import React from "react";
import Enzyme, {mount} from "enzyme";
import Adapter from "enzyme-adapter-react-16";

import Users from "../src/Users";

Enzyme.configure({adapter: new Adapter()});

describe("Users", () => {
  let wrapper;
  let users;
  
  beforeEach(() => {
    const mockResponseData = [{id: 1}, {id: 2}, {id: 3}];
    users = mockResponseData.map(e => ({...e}));
    jest.clearAllMocks();
    global.fetch = jest.fn(async () => ({
      json: async () => mockResponseData
    }));
    wrapper = mount(<Users />);
  });
  
  it("renders a count of users", async () => {
    //                           ^^^^^
    await act(() => new Promise(setImmediate)); // <--
    wrapper.update();                           // <--
    const p = wrapper.find("p");
    expect(p.exists()).toBe(true);
    expect(p.text()).toEqual("there are 3 users");
  });
});

The new Promise(setImmediate) technique also helps us assert on state before the promise resolves. act (from react-dom/test-utils) is necessary to avoid Warning: An update to Users inside a test was not wrapped in act(...) that pops up with useEffect.

Adding this test to the above code also passes:

it("renders a count of 0 users initially", () => {
  return act(() => {
    const p = wrapper.find("p");
    expect(p.exists()).toBe(true);
    expect(p.text()).toEqual("there are 0 users");
    return new Promise(setImmediate);
  });
});

The test callback is asynchronous when using setImmediate, so returning a promise is necessary to ensure Jest waits for it correctly.

This post uses Node 12, Jest 26.1.0, Enzyme 3.11.0 and React 16.13.1.

Solution 2:[2]

With jest you can always mock. So what you need is:

  1. In your unit test mock useEffect from React
jest.mock('React', () => ({
  ...jest.requireActual('React'),
  useEffect: jest.fn(),
}));

That allows to mock only useEffect and keep other implementation actual.

  1. Import useEffect to use it in the test
import { useEffect } from 'react';
  1. And finally in your test call the mock after the component is rendered
useEffect.mock.calls[0](); // <<-- That will call implementation of your useEffect

Solution 3:[3]

useEffect has already been triggered and working, the point is that its an async operation. So you need to wait for the fetch to be completed. one of the ways that you can do that is: 1. write your assertion(s) 2. specify the number of assertion(s) in your test, so that jest knows that it has to wait for the operation to be completed.

 it('handles groupId and api call ', () => {
    // the effect will get called
    // the effect will call getGroups
    // the iframe will contain group parameters for the given groupId
    expect.assertions(1)
    const wrapper = shallow(<UsageWidget surface={`${USAGE_SURFACES.metrics}`} groupId={2} />)
    wrapper.update()
    expect(whatever your expectation is)
  });

since in this example i just wrote on assertion,

expect.assertions(1)

if you write more, you need to change the number.

Solution 4:[4]

You can set a timeout to asynchronously check if the the expected condition has been met.

it('handles groupId and api call ', (done) => {
  const wrapper = shallow(<UsageWidget surface={`${USAGE_SURFACES.metrics}`} groupId={1} />) 
  setTimeout(() => {
    expect(wrapper.find("iframe").prop('src')).toBeTruthy(); // or whatever
    done();
  }, 5000);
}

The timeout lets you wait for the async fetch to complete. Call done() at the end to signal that the it() block is complete.

You probably also want to do a mock implementation of your getGroups function so that you're not actually hitting a network API every time you test your code.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 Max
Solution 3 meisam
Solution 4 jered