How to test react-toastify with jest and react-testing-library
Asked Answered
H

4

10

I have a screen with some form, and on submission, I send the request to back-end with axios. After successfully receiving the response, I show a toast with react-toastify. Pretty straight forward screen. However, when I try to test this behavior with an integration test using jest and react testing library, I can't seem to make the toast appear on DOM.

I have a utility renderer like that to render the component that I'm testing with toast container:

import {render} from "@testing-library/react";
import React from "react";
import {ToastContainer} from "react-toastify";

export const renderWithToastify = (component) => (
  render(
    <div>
      {component}
      <ToastContainer/>
    </div>
  )
);

In the test itself, I fill the form with react-testing-library, pressing the submit button, and waiting for the toast to show up. I'm using mock service worker to mock the response. I confirmed that the response is returned OK, but for some reason, the toast refuses to show up. My current test is as follows:

expect(await screen.findByRole("alert")).toBeInTheDocument();

I'm looking for an element with role alert. But this seems to be not working. Also, I tried doing something like this:

...

beforeAll(() => {
  jest.useFakeTimers();
}

...

it("test", () => {
  ...

  act(() =>
    jest.runAllTimers();
  )
  expect(await screen.findByRole("alert")).toBeInTheDocument();
}

I'm kind of new to JS, and the problem is probably due to asynch nature of both axios and react-toastify, but I don't know how to test this behavior. I tried a lot of things, including mocking timers and running them, mocking timers and advancing them, not mocking them and waiting etc. I even tried to mock the call to toast, but I couldn't get it working properly. Plus this seems like an implementation detail, so I don't think I should be mocking that.

I think the problem is I show the toast after the axios promise is resolved, so timers gets confused somehow.

I tried to search many places, but failed to find an answer.

Thanks in advance.

Helmuth answered 17/11, 2020 at 12:18 Comment(1)
This way you end up testing someone else's lib in your own tests while it may work improperly in test environment, be ready to mock it. It's unknown what's going on in your component so it's impossible to say exactly how it should be tested. See stackoverflow.com/help/how-to-ask and stackoverflow.com/help/mcve . Promises aren't 'timers' so they surely aren't flushed with runAllTimers. Try flush-promises lib to wait a bit more, in case you did everything else correctly this will be enough for a promise to take effect. Otherwise use RTL's waitFor for looser control flow.Pokorny
H
23

Thank you @Estus Flask, but the problem was much much more stupid :) I had to render ToastContainer before my component, like this:

import {render} from "@testing-library/react";
import React from "react";
import {ToastContainer} from "react-toastify";

export const renderWithToastify = (component) => {
  return (
    render(
      <div>
        <ToastContainer/>
        {component}
      </div>
    )
  );
};

Then, the test was very simple, I just had to await on the title of the toast:

expect(await screen.findByText("alert text")).toBeInTheDocument();

The findByRole doesn't seem to work for some reason, but I'm too tired to dig deeper :) I didn't have to use any fake timers or flush the promises. Apperently, RTL already does those when you use await and finBy* queries, only the order of rendering was wrong.

Helmuth answered 17/11, 2020 at 14:10 Comment(0)
T
13

In order to use a mock when you don't have access to the DOM (like a Redux side effect) you can do:

import { toast } from 'react-toastify'

jest.mock('react-toastify', () => ({
  toast: {
    success: jest.fn(),
  },
}))
expect(toast.success).toHaveBeenCalled()
Tacy answered 22/9, 2022 at 17:29 Comment(0)
B
-1

What I would do is mock the method from react-toastify to spy on that method to see what is gets called it, but not the actual component appearing on screen:

// setupTests.js
jest.mock('react-toastify', () => {
  const actual = jest.requireActual('react-toastify');
  Object.assign(actual, {toast: jest.fn()});
  return actual;
});

and then in the actual test:

// test.spec.js
import {toast} from 'react-toastify';

const toastCalls = []
const spy = toast.mockImplementation((...args) => {
     toastCalls.push(args)
  }
)

describe('...', () => {
  it('should ...', () => {
    // do something that calls the toast
    ...
    // then
    expect(toastCalls).toEqual(...)
   }
 }
)


Another recommendation would be to put this mockImplementation into a separate helper function which you can easily call for the tests you need it for. This is a bear bones approach:


function startMonitoring() {
  const monitor = {toast: [], log: [], api: [], navigation: []};

  toast.mockImplementation((...args) => {
    monitor.toast.push(args);
  });
  log.mockImplementation((...args) => {
    monitor.log.push(args);
  });
  api.mockImplementation((...args) => {
    monitor.api.push(args);
  });
  navigation.mockImplementation((...args) => {
    monitor.navigation.push(args);
  });
  return () => monitor;
}

it('should...', () => {
  const getSpyCalls = startMonitoring();
  // do something

  expect(getSpyCalls()).toEqual({
    toast: [...],
    log: [...],
    api: [...],
    navigation: [...]
  });
});

Byron answered 24/11, 2020 at 20:41 Comment(0)
G
-2

Here, the solution was use getByText:

await waitFor(() => {
  expect(screen.getByText(/Logged!/i)).toBeTruthy()
})
Genevivegenevra answered 14/6, 2021 at 19:41 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.