How to unit test the timer functions – setTimeout and setInterval in React using jest

When we are using timer functions like setTimeout or setInterval, it is difficult to test them as the callback function is called after a certain time is passed. In async http API calls, we can mock the API call and test the success and error condition. In async functions like setTimeout or setInterval, we can also mock the timing to make it speed faster instead of waiting for it to over. Let us see how.

import React, { Component } from "react";
class Timers extends Component {
  constructor() {
    super();
    this.state = {
      index: 0
    };
    this.setTimeoutFn = this.setTimeoutFn.bind(this);
    this.setIntervalFn = this.setIntervalFn.bind(this);
  }
  setTimeoutFn() {
    setTimeout(() => {
      this.setState({
        index: 1
      });
    }, 1000);
  }
  setIntervalFn() {
    setInterval(() => {
      this.setState({
        index: this.state.index + 1
      });
    }, 1000);
  }
  render() {
    return <div></div>;
  }
}
export default Timers;

In the above function, there are two functions - setTimeoutFn which starts the setTimeout and setIntervalFn which starts the setInterval. We have a state variable that is being updated in the callback function of the setTimeout and setInterval. Let us see the unit test code using the enzyme.

import React from "react";
import { shallow } from "enzyme";
import Timers from "../Timers";
describe("settimeout fn", () => {
  it("should increment index by 1 after 1 second (using advanceTimersByTime)", () => {
    const component = shallow(<Timers />);
    jest.useFakeTimers();
    expect(component.state("index")).toEqual(0);
    component.instance().setTimeoutFn();
    jest.advanceTimersByTime(1000);
    expect(component.state("index")).toEqual(1);
    jest.useRealTimers();
  });
  it("should increment index by 1 after 1 second (using runAllTimers)", () => {
    const component = shallow(<Timers />);
    jest.useFakeTimers();
    expect(component.state("index")).toEqual(0);
    component.instance().setTimeoutFn();
    jest.runAllTimers(); // you can also use run.runOnlyPendingTimers()
    expect(component.state("index")).toEqual(1);
    jest.useRealTimers();
  });
});
describe("setinterval fn", () => {
  it("should increment index by 1 after 1 second (using advanceTimersByTime)", () => {
    const component = shallow(<Timers />);
    jest.useFakeTimers();
    expect(component.state("index")).toEqual(0);
    component.instance().setIntervalFn();
    jest.advanceTimersByTime(1000);
    expect(component.state("index")).toEqual(1);
    jest.useRealTimers();
  });
  it("should increment index by 1 after 1 second (using runOnlyPendingTimers)", () => {
    const component = shallow(<Timers />);
    jest.useFakeTimers();
    expect(component.state("index")).toEqual(0);
    component.instance().setIntervalFn();
    jest.runOnlyPendingTimers(); // using runAllTimers will be error as setInterval never
    // finishes. So it will be infinite loop.
    expect(component.state("index")).toEqual(1);
    jest.useRealTimers();
  });
});

Conclusion

The following points are to be noted from the above unit test code.

There is no need to wait for the interval time to be over to test the callback function code. We can use fake timers to advance the time. Following is the code to use the fake timers.

jest.useFakeTimers();

The time can be advanced using the following code.

jest.advanceTimersByTime(1000);

For testing the interval function callback code, we use runPendingTimers. If we use something like runAllTimers to wait for all the timers to run, then it will result in an infinite loop in setInterval.

jest.runOnlyPendingTimers(); 

If at any point, you want to revert back to using real timers, then use the following code.

jest.useRealTimers();