0

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:

  • Simulating button click.
  • Simulating lifecycle method.

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'.

1 Answer 1

1
  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
  });
});

Sign up to request clarification or add additional context in comments.

2 Comments

Hi @skyboyer Thanks for such a detailed explanation. I knew testing on instance was quite pointless. I was just going through a random tutorial on jest and enzyme. Anyway, I've updated the description and added the definition for fetchRandom method. I can't call mockReturnValue on it.
sorry, I don't understand "I can't call" in that context. have you added jest.mock( "../resources/api"); as I showd above? If you did, fetchRandom should definitely have mockReturnValue method

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.