Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Trigger useEffect in Jest 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

like image 850
Batman Avatar asked Dec 02 '19 22:12

Batman


People also ask

How do you test the useEffect?

To test the component update useEffect hook you'd simply trigger state updates and check for effects in rendered elements. Redux hooks can be tested by mocking them and their implementation.

How do you mock react State in jest?

To enable us to mock useState, we use React. useState (line #5) instead of using the usual named import (i.e. import { useState } from 'react'). Below is our Jest unit test for the component. Before rendering the component for testing, we create a constant 'setStateMock' and mock it with a jest function jest.


2 Answers

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 since it doesn't exist in Node:

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.

like image 101
ggorlen Avatar answered Oct 13 '22 19:10

ggorlen


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
like image 36
Max Avatar answered Oct 13 '22 18:10

Max