I have a notifications component that has some defaults for a toaster. When running tests against it using a jest mock implementation on useState
, I’m receiving some strange errors.
Notifications.tsx
import { Toaster } from "react-hot-toast";
export const Notifications = () => {
return (
<Toaster
position="top-center"
containerStyle={{ top: 30 }}
gutter={10}
toastOptions={{
style: {
minWidth: "1000px",
},
success: {
style: {
backgroundColor: "#e0fce0",
},
},
error: {
style: {
backgroundColor: "#fce0e0",
},
},
duration: Infinity,
}}
/>
);
};
Page
Using the component (complexity reduced).
import { useContext, useEffect, useState } from "react";
import { Notifications } from "../components/Notifications";
export default function Page() {
const [selected, useSelected] = useState("default");
return (
<>
<Notifications />
<div>
Same content
</div>
</>
);
}
Page.test.tsx
import { render, waitFor } from "@testing-library/react";
import Page from "../Page";
import React from "react";
jest.mock("../../components/Notifications/index", () => () => (
<div>Toaster</div>
));
describe("Page", () => {
let stateSpy: any;
beforeEach(() => {
stateSpy = jest.spyOn(React, "useState");
});
afterEach(() => {
stateSpy.mockReset();
});
it("should load the page with defaults", async () => {
render(
<Page />
);
await waitFor(() => {
// Run some tests
});
});
it("should load the page with a second value", async () => {
//@ts-ignore
stateSpy.mockImplementationOnce(() => ["second value", jest.fn()]);
render(
<Page />
);
await waitFor(() => {
// Run some tests
});
});
it("should load the page with a third value", async () => {
//@ts-ignore
stateSpy.mockImplementationOnce(() => ["third value", jest.fn()]);
render(
<Page />
);
await waitFor(() => {
// Run some tests
});
});
it("should load the page with a fourth value", async () => {
//@ts-ignore
stateSpy.mockImplementationOnce(() => ["fourth value", jest.fn()]);
render(
<Page />
);
await waitFor(() => {
// Run some tests
});
});
});
On the first test (using the default value for state), I receive the following error on the render
:
Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: undefined. You likely forgot to export your component from the file it's defined in, or you might have mixed up default and named imports.
On the latter three tests (setting a value for state), I receive the following error on the line const [selected, useSelected] = useState("default");
(in the Page.tsx):
TypeError: undefined is not iterable (cannot read property Symbol(Symbol.iterator))
If I change mockImplementationOnce
to mockImplementation
on the latter three tests, then all four tests generate the first error (Element type is invalid...
).
If I remove the reference to the <Notifications />
component (or just comment out the <Toaster />
component in it), then the tests run fine. You’ll notice that I’m trying to mock the notifications component in the test. But, even mocking it doesn’t seem to make a difference. I still get the errors regardless.
Just FYI (not sure if it makes a difference), but <Toaster />
is a functional component (React.FC), not a JSX element.
Fixed
I thought I was mocking the Notification component, however, the paths in the Page.tsx
and Page.test.tsx
for the component were different (e.g., "./components/Notifications"
vs. "../../components/Notifications/index"
). When I removed the trailing /index
in the test. Everything worked as expected.
I don’t think I’ve ever seen a react test that spies on
useState
. That seems like a bad idea. If you want to change the state in a test, then you should interact with the component in the way that changes that state.@AlexWayne there is a child component that updates state (lifting state). Then other child components of the page take advantage of the changed state. Additionally, what’s rendered is changed based on state. Given that this is a unit test, I need to eliminate dependencies on child components.
Well the “complexity reduced” example has been reduced to not actually use any of the values returned by
useState
so it’s hard to know how to recommend an alternative. If the state is completely internal, then you don’t need to mock that. If its manipulatable from the outside, then that interface is what what you should be testing.Got it working. Added an answer to the fix. Thanks for your help, nonetheless.