saran3h
saran3h

Reputation: 14022

How to test a React component having async functions with jest & enzyme

I have a component:

RandomGif.js

import React, { Component } from "react";
import Gif from "./Gif";
import Loader from "./library/Loader";
import { fetchRandom } from "../resources/api";

class RandomGif extends Component {
  constructor(props) {
    super(props);

    this.handleClick = this.handleClick.bind(this);
  }

  state = {
    loading: false,
    gif: null
  };

  componentDidMount() {
    this.handleClick();
  }

  async handleClick() {
    let gifContent = null;

    try {
      this.setState({
        loading: true
      });

      const result = await fetchRandom();

      if (!!result && result.data) {
        gifContent = {
          id: result.data.id,
          imageUrl: result.data.images.downsized_large.url,
          staticImageUrl: result.data.images.downsized_still.url,
          title: result.data.title
        };
      }
    } catch (e) {
      console.error(e);
    } finally {
      this.setState({
        loading: false,
        gif: gifContent
      });
    }
  }

  render() {
    const { gif, loading } = this.state;

    const showResults = gif && !loading;

    return (
      <div className="random">
        {!showResults && <Loader />}

        <button className="btn" onClick={this.handleClick}>
          RANDOMISE
        </button>

        {showResults && <Gif data={gif} />}
      </div>
    );
  }
}

export default RandomGif;

If I call methods directly from the instance of this component, I can successfully test that the state is being updated. However, If I simulate a button click, nothing gets updated and the test fails. I've tried setImmediate and setTimeout tricks but those are not working.

So far I've not able to write a test case for:

This is what I've come up with so far.

RandomGif.spec.js

import React from "react";
import { shallow, mount } from "enzyme";
import RandomGif from "./RandomGif";

describe("Generate Random Gif", () => {
  it("should render correctly.", () => {
    const wrapper = shallow(<RandomGif />);

    expect(wrapper).toMatchSnapshot();
  });

  it("should load a random GIF on calling handleSearch fn.", async () => {
    const wrapper = mount(<RandomGif />);
    const instance = wrapper.instance();

    expect(wrapper.state("gif")).toBe(null);

    await instance.handleClick();

    expect(wrapper.state("gif")).not.toBe(null);
  });

  it("THIS TEST FAILS!!!", () => {
    const wrapper = mount(<RandomGif />);

    expect(wrapper.state("gif")).toBe(null);

    wrapper.find('button').simulate('click');
    wrapper.update()

    expect(wrapper.state("gif")).not.toBe(null);
  });
});

api.py

export const fetchRandom = async () => {
  const url = `some_url`;

  try {
    const response = await fetch(url);

    return await response.json();
  } catch (e) {
    console.error(e);
  }

  return null;
};

Please help me figure out the missing pieces of a puzzle called 'frontend testing'.

Upvotes: 0

Views: 3190

Answers (1)

skyboyer
skyboyer

Reputation: 23705

  1. We need to mock fetchRandom so no real request will be sent during testing.
import { fetchRandom } from "../resources/api";

jest.mock("../resources/api"); // due to automocking fetchRandom is jest.fn()

// somewhere in the it()
fetchRandom.mockReturnValue(Promise.resolve({ data: { images: ..., title: ..., id: ...} }))
  1. Since mocking is a Promise(resolved - but still promise) we need either setTimeout or await <anything> to make component's code realized this Promise has been resolved. It's all about microtasks/macrotasks queue.
wrapper.find('button').simulate('click');
await Promise.resolve();
// component has already been updated here

or

it("test something" , (done) => {
wrapper.find('button').simulate('click');
setTimeout(() => {
  // do our checks on updated component
  done();  
}); // 0 by default, but it still works

})

Btw you've already did that with

await instance.handleClick();

but to me it looks the same magic as say

await 42;

And besides it works(look into link on microtasks/macrotasks) I believe that would make tests worse readable("what does handleClick return that we need to await on it?"). So I suggest use cumbersome but less confusing await Promise.resolve(); or even await undefined;

  1. Referring to state and calling instance methods directly are both anti-patterns. Just a quote(by Kent C. Dodds I completely agree with):

In summary, if your test uses instance() or state(), know that you're testing things that the user couldn't possibly know about or even care about, which will take your tests further from giving you confidence that things will work when your user uses them.

Let's check rendering result instead:

import Loader from "./library/Loader";

...
wrapper.find('button').simulate('click');
expect(wrapper.find(Loader)).toHaveLength(1);
await Promise.resolve();
expect(wrapper.find(Loader)).toHaveLength(1);
expect(wrapper.find(Gif).prop("data")).toEqual(data_we_mocked_in_mock)

Let's get that altogether:

import {shallow} from "enzyme";
import Gif from "./Gif";
import Loader from "./library/Loader";
import { fetchRandom } from "../resources/api";

jest.mock( "../resources/api");

const someMockForFetchRandom = { data: { id: ..., images: ..., title: ... }};

it("shows loader while loading", async () => {
  fetchRandom.mockReturnValue(Promise.resolve(someMockForFetchRandom));
  const wrapper = shallow(<RandomGif />);
  expect(wrapper.find(Loader)).toHaveLength(0);
  wrapper.find('button').simulate('click');
  expect(wrapper.find(Loader)).toHaveLength(1);
  await Promise.resolve();
  expect(wrapper.find(Loader)).toHaveLength(0);
});

it("renders images up to response", async () => {
  fetchRandom.mockReturnValue(Promise.resolve(someMockForFetchRandom));
  const wrapper = shallow(<RandomGif />);
  wrapper.find('button').simulate('click');
  expect(wrapper.find(Gif)).toHaveLength(0);
  await Promise.resolve();
  expect(wrapper.find(Gif).props()).toEqual( {
    id: someMockForFetchRandom.data.id,
    imageUrl: someMockForFetchRandom.data.images.downsized_large.url,
    staticImageUrl: someMockForFetchRandom.data.images.downsized_still.url,
    title: someMockForFetchRandom.data.title
  });
});

Upvotes: 1

Related Questions