Testing Async React Server Components in a Next.js Project
2024-08-13
2024-08-13
Testing async components in a Next.js project can be tricky, particularly when dealing with React Server Components. The challenge arises from the need to handle asynchronous data fetching and Suspense boundaries properly.
React Server Components allow you to fetch data on the server and send it to the client, enhancing performance by reducing the amount of JavaScript required on the client-side. However, this asynchrony introduces complexities in testing.
Having asynchronous components like this introduces some challenges when writing unit tests
export type PageSearchParams = {
query?: string;
page?: string;
};
export default async function Page({
searchParams,
}: {
searchParams?: PageSearchParams;
}) {
const query = searchParams?.query || "";
const currentPage = Number(searchParams?.page) || 1;
const asyncData = await fetchSomeDataAsynchronously(query, currentPage);
return (
<>Do something with {...asyncData}</>
);
}
A common issue is that when testing async components, you might encounter empty snapshots. This is illustrated by the following example, which renders an async component but ends up with an empty snapshot:
test("This test will pass but it will generate an empty snapshot", async () => {
const { container } = render(<Page />);
expect(container).toMatchSnapshot();
});
The generated snapshot might look like this:
exports[`This test will pass but it will generate an empty snapshot 1`] = `<div />`;
If we try to perform some screen
assertions, the test will fail. For example:
test("This test will fail because the string cannot be found", async () => {
const { container } = render(<Page />);
await screen.findByText("Some text in your page"); // can't be found
});
Please note that, depending on the version of Next.js you are using, you might encounter errors like this:
Error: Objects are not valid as a React child (found: [object Promise]). If you meant to render a collection of children, use an array instead.
Check the links at the end of the post to see which package versions should help avoid these errors.
<Suspense>
and screen
To solve this issue I discovered a workaround in this GitHub issue.
The workaround is to wrap your Component with <Suspense>
to the render call. With that change the screen
assert will pass.
test("This will pass but will now pass", async () => {
const { container } = render(
<Suspense>
<Page />
</Suspense>,
);
await screen.findByText("Some text in your page");
});
From the other side, if you are only interested in asserting the snapshot like this, the snapshot will still be empty.
test("This will pass but will still generate an empty snapshot", async () => {
const { container } = render(
<Suspense>
<Page />
</Suspense>,
);
expect(container).toMatchSnapshot();
});
I discovered that for some reason that I cannot understand, if you call first a screen
assert the snapshot will finally generate a correct snapshot.
test("This will pass and will now generate a correct snapshot", async () => {
const { container } = render(
<Suspense>
<Page />
</Suspense>,
);
await screen.findByText("Some text in your page");
expect(container).toMatchSnapshot();
});
As far as I understand, the issues are caused by these features being quite new and still in development, and it is possible that by the time you read this, the issue may already be resolved.
Please note that my proposed solutions are more of a workaround and may rely on unstable package versions.
In any case, I hope this helps others successfully write unit tests for asynchronous server components ✅ 🚀.
You can check the source code of this blog to see these workarounds in action within a real-world app, or review this demo project that showcases the problem with a simple app.
Happy coding! Happy testing!