2

I am trying to mock my axios get in Jest. I'm new to testing in react so please be kind

I've followed this tutorial and this answer, I think i've got a grip of what to do. Basically on component mounting, I called 2 data fetching on my UseEffect.

Here's the not working test:

jest.mock("axios");

describe("User form async tests", () => {

  it("Renders widget", async (done) => {
  const promiseRole = new Promise((resolve, reject) =>
  ┊ setTimeout(
  ┊ ┊ () =>
  ┊ ┊ ┊ resolve({
  ┊ ┊ ┊ ┊ data: [
  ┊ ┊ ┊ ┊ ┊ { id: 0, name: "Admin" },
  ┊ ┊ ┊ ┊ ┊ { id: 1, name: "Editor" },
  ┊ ┊ ┊ ┊ ],
  ┊ ┊ ┊ }),
  ┊ ┊ 100
  ┊ )
  );

  const promiseLocs = new Promise((resolve, reject) =>
  ┊ setTimeout(
  ┊ ┊ () =>
  ┊ ┊ ┊ resolve({
  ┊ ┊ ┊ ┊ data: [
  ┊ ┊ ┊ ┊ ┊ { id: 0, name: "Loc1" },
  ┊ ┊ ┊ ┊ ┊ { id: 1, name: "Loc2" },
  ┊ ┊ ┊ ┊ ],
  ┊ ┊ ┊ }),
  ┊ ┊ 100
  ┊ )
  );

  ┊ axios.get.mockImplementationOnce(() => promiseRole);
  ┊ axios.get.mockImplementationOnce(() => promiseLocs);
  ┊ const wrapper = mount(<UserForm />);
  ┊ await promiseRole;
  ┊ await promiseLocs;

  ┊ setImmediate(() => {
  ┊ ┊ wrapper.update();
  ┊ ┊ expect(wrapper.contains(Widget)).toEqual(true);

  ┊ ┊ done();
  ┊ });
  });
});

Somehow it always returns false for the widget. After the data is fetched, what I did was I set the state to ready, thus it will render the Widget component. Interestingly, if i changed the code to mockResolvedValue straightaway then it works. What I don't understand is why the code above does not work? Basically it resolve the same value as well... can anyone please help? I'm really confused to get my mind around this and it is really bothering me

If axios returns the correct data, there is a code that calls setReady(true), thus it returns <Widget ... /> after

Thanks

Working test because I mocked every get request:


jest.mock("axios");

describe("User form async tests", () => {

  it("Renders widget", async (done) => {
  ┊ const location = [{ id: 0, name: "hurstville" }];
  ┊ const resp = { data: location };
  ┊ axios.get.mockResolvedValue(resp);
  ┊ const wrapper = mount(<UserForm />);

  ┊ setImmediate(() => {
  ┊ ┊ wrapper.update();
  ┊ ┊ expect(wrapper.contains(Widget)).toEqual(true);

  ┊ ┊ done();
  ┊ });
  });
});

In UserForm component I'm using formik and material-ui. So the problem lies at the getData() function in useEffect where it expect data type of [{id: PropTypes.number, name: PropTypes.string}] to be provided in <FormikSelect /> component for selection.

Thus the second working test works

UserForm:

import React, { useEffect, useState } from "react";
import {
  FormikTextField,
  FormikArray,
  FormikSelect,
} from "app/components/forms/Utils";
import { FormikTransferList, Widget, Alert } from "app/components";
import { Formik, Form, ErrorMessage } from "formik";
import {
  getUserById,
  getUserRole,
  addUser,
  getLocationList,
  updateUser,
} from "app/crud/user.crud";
import {
  formValues,
  validationUser,
} from "app/components/forms/users/UserConst";
import { useLocation, useHistory } from "react-router-dom";
import FuseSplashScreen from "@fuse/core/FuseSplashScreen";
import { Snackbar, Button } from "@material-ui/core";

const processedLocs = (data) =>
  data.map((item) => {
    return {
      ...item,
      key: item.id,
      title: item.name,
    };
  });

function UserForm() {
  const [initial, setFormValues] = useState(formValues);
  const location = useLocation();
  const history = useHistory();
  const [roles, setRoles] = useState([]);
  const [locationList, setLocationList] = useState([]);
  const [ready, setReady] = useState(false);
  const [open, setOpen] = useState(false);

  useEffect(() => {
    async function getData() {
      try {
        const response = await getUserRole();
        const locresult = await getLocationList();
        if (response) {
          setRoles(response.data);
          setLocationList(processedLocs(locresult.data));
          setReady(true);
        }
      } catch (err) {
        console.log(err.message);
      }
    }

    getData();
  }, [getUserRole]);

  useEffect(() => {
    async function getUserDetails(id) {
      const response = await getUserById(id);
      if (response) {
        const newVal = {
          ...response.data,
          roles: response.data.roles.map((item) => ({
            ...item,
            locations: item.locations.map((loc) => loc.id),
          })),
        };
        setFormValues(newVal);
      } else {
        setFormValues(formValues);
      }
    }
    if (location.state && location.state.id) {
      getUserDetails(location.state.id);
    }
  }, [location.state]);

  const handleCloseAlert = (event, reason) => {
    if (reason === "clickaway") {
      return;
    }
    setOpen(false);
  };

  const handleSuccess = () => {
    setOpen(true);
  };

  return ready ? (
    <>
      <Formik
        enableReinitialize
        initialValues={initial}
        validationSchema={validationUser}
        onSubmit={async (
          values,
          { setSubmitting, setErrors, setStatus, resetForm }
        ) => {
          setTimeout(async () => {
            try {
              let response;
              if (location.state && location.state.id) {
                response = await updateUser(values.id, values);
              } else {
                response = await addUser(values);
                if (response) {
                  handleSuccess();
                }
              }
              resetForm(formValues);
              history.goBack();
              setStatus({ success: true });
            } catch (err) {
              setSubmitting(false);
              setErrors({ submit: err.message });
            }
          }, 200);
        }}
      >
        <Form>
          <div className="container grid grid-cols-1 gap-4 p-8">
            <Widget title="User Details" containerClass="p-8">
              <div className="grid grid grid-cols-1 gap-4">
                <FormikTextField
                  id="name"
                  variant="outlined"
                  name="name"
                  label="Full Name"
                />
                <FormikTextField
                  id="email"
                  variant="outlined"
                  name="email"
                  label="Email Address"
                />
                <FormikTextField
                  id="phone"
                  variant="outlined"
                  name="phone"
                  label="Phone number"
                />
              </div>
            </Widget>
            <Widget title="User Roles" containerClass="p-8">
              <div className="flex flex-col p-8">
                <ErrorMessage
                  name="roles"
                  render={(msg) => (
                    <div className="text-lg text-red-600 p-8 ml-8">{msg}</div>
                  )}
                />
                <FormikArray name="roles" defaultValue={initial.roles[0]}>
                  {({ index }) => (
                    <div className="flex flex-col p-8 mb-8">
                      <div>
                        <p className="text-lg font-semibold text-blue-400">
                          Assign locations to the role of the user. Each user
                          can has multiple locations based on the role that they
                          have
                        </p>
                      </div>
                      <div>
                        <p className="text-lg font-medium">Role {index + 1} </p>
                      </div>
                      <FormikSelect
                        labelId={`rolesSelectLabel${index}`}
                        options={roles}
                        label="Select Roles"
                        selectId={`rolesSelect${index}`}
                        name={`roles.${index}.id`}
                      />
                      <div>
                        <p className="text-lg font-medium">Put Location:</p>
                      </div>
                      <div className="w-full">
                        <FormikTransferList
                          options={locationList}
                          name={`roles.${index}.locations`}
                        />
                      </div>
                    </div>
                  )}
                </FormikArray>
              </div>
              <div className="w-1/2 p-8">
                <Button variant="contained" color="primary" type="submit">
                  {location.state && location.state.id ? "Update" : "Submit"}
                </Button>
              </div>
            </Widget>
          </div>
        </Form>
      </Formik>
      <Snackbar open={open} autoHideDuration={4000} onClose={handleCloseAlert}>
        <Alert onClose={handleCloseAlert} severity="success">
          User Created
        </Alert>
      </Snackbar>
    </>
  ) : (
    <FuseSplashScreen />
  );
}

UserForm.propTypes = {};

export default UserForm;
DevBF
  • 247
  • 4
  • 15
  • You didn't post the code you're testing. These are two different mocks that work differently. The first one expects 2 different requests. The second one expects unlimited amount of location requests. – Estus Flask Jun 29 '20 at 05:58
  • @EstusFlask The top code is the code that I was testing. Since both request provides the same propTypes, when I mocked it into 1 type it just works for both data fetching. Originally I wanted to mock both of the fetching request thus the top code is the preferred one. `data: [{id: number, name: string}]` – DevBF Jun 30 '20 at 00:25
  • The question right now is why is the top one not working in terms of the axios mocking value. The second one mocks correctly, thus the `setReady(true)` is called. I edited my question so its clear. – DevBF Jun 30 '20 at 00:26
  • The question doesn't contain UserForm. Please, provide it instead of describing it. The problem is specific to what happens in your component. See https://stackoverflow.com/help/mcve – Estus Flask Jun 30 '20 at 05:54
  • Sorry, okay I add it @EstusFlask, I just thought that you wouldn't need it because I provide the working test that expect the same data type. There are too many dependencies that it wouldn't make much more sense. Edited my question again – DevBF Jun 30 '20 at 23:02
  • The question actually is pretty clear and specific that my **not working** test does not return the promise, while the working test successfully returns the data (Because I mocked everything instead of each request) – DevBF Jun 30 '20 at 23:06
  • Yes, it's better to keep the example minimal but being able to reproduce the problem, this is the purpose of MCVE. The component doesn't explain why there's a problem. If there are errors, they should have appeared in console. Since there's no MCVE, you can try to debug it yourself. There are several discrepancies that need to be checked. Make sure it's called twice as expected, `toBeCalledTimes(2)`, because it may be not, crud functions weren't shown. I don't see why a delay could affect anything here but it can, check if setTimeout 100 affects the result. – Estus Flask Jul 01 '20 at 07:28
  • Yeah I don't see why the setTimeout affects it because I already call the promise with await. I'll try to strip down my component and see if I can give example that can be replicated. Maybe its just not the right way to mock implementation with promise? – DevBF Jul 01 '20 at 23:15
  • No, the mock is ok, if it needs small setTimeout as a workaround then do it but usually Promise.resolve alone is fine. There may be race condition, it can be introduced not by your code but Formik. FWIW, native promises are fast, if there's some delay like setTimeout 0, it will be slower than multiple await without setTimeouts. – Estus Flask Jul 02 '20 at 09:06

0 Answers0