Timers
Last modified on Tue 21 May 2024

Introduction to Testing Timers

JavaScript provides several timing functions that can be used to execute code after a specified period of time or at regular intervals. The most commonly used timers are setTimeout and setInterval. - Tatiana Maslyak, Mastering Asynchronous Timing

Common use-cases for timeouts

Using timeouts is a common practice, often driven by the need to manage or introduce delays in component behaviors, handle asynchronous operations, or enhance user experience. Here are some typical use-cases for using timeouts in React:

Remember, these are just a few scenarios. While timeouts can be handy in managing these scenarios, it's essential to handle them correctly, ensuring they are cleared appropriately (using clearTimeout) to avoid unwanted side-effects or memory leaks, especially when components unmount.

The Challenges of Testing Components with Timeouts:

Testing components with timeouts introduces specific challenges that can make the process trickier than testing synchronous operations or even other types of asynchronous behaviors. Let's delve into the challenges posed by timeouts:

Properly mocking and controlling timeouts can lead to more consistent, faster, and reliable tests.

Utilizing Testing Libraries and Tools

Jest's Timer Functions

React Testing Library

Examples

Using waitForElementToBeRemoved()

A simple example is waiting for our component to fetch an external resource from an API endpoint (We are using MSW to mock the endpoint). waitForElementToBeRemoved function from React Testing Library, which awaits for the queryByText to return true.

it("should show empty state", async () => {
  server.use(
    rest.get("/api/lists", (req, res, ctx) => {
      return res(ctx.json([]));
    })
  );

  render(<TodoLists />);

  await waitForElementToBeRemoved(() => screen.queryByText("Loading..."));

  expect(screen.findByText("Empty List!"));
});

Waiting for setTimeout()

Here we are testing a full flow of a User creating a new Todo Item. The component displays if the user has successfully submitted the form and renders a toast notification using the setTimeout() function and removes the notification from the DOM after 3 seconds.

jest.useFakeTimers();
jest.spyOn(global, "setTimeout");

it("should add item", async () => {
  const user = userEvent.setup({ delay: null }); // Important to include

  render(<NewItemForm />);

  await waitForElementToBeRemoved(() => screen.queryByText("Loading..."));

  await user.type(screen.getByPlaceholderText("Title"), "List 1");
  await user.type(screen.getByPlaceholderText("Task title"), "Task 1");

  await user.click(
    within(createModal).getByRole("button", { name: /Create/i })
  );

  expect(
    screen.getByText("Successfully Created New List Item")
  ).toBeInTheDocument();

  // Fast-forward until all timers have been executed
  act(() => jest.runAllTimers());

  expect(
    screen.queryByText("Successfully Created New List Item")
  ).not.toBeInTheDocument();
});

Best Practices

By making the most of the tools at your disposal and adhering to best practices, you can write reliable, fast, and comprehensive tests for asynchronous behavior in your components.

Suggested Reading