I’ve successfully tested one isolated server component by using an async it
and awaiting the output of a call to the component as a plain function to pass into render:
it('renders the user data when user is authenticated', async () => {
render(await UserData())
expect(screen.getByTestId('user-data')).toBeInTheDocument();
});
^ That works and passes.
But I also have a page.tsx serever component that renders <UserData />
nested within it:
'use server'
import styles from './page.module.css';
import LogInOut from "./LogInOut/LogInOut";
import UserData from "./UserData";
export default async function Page() {
return (
<main className={styles.main}>
<h1>Title</h1>
<LogInOut/>
<UserData />
</main>
);
}
// Note: LogInOut is a client component, removing it doesn't help the problem.
… and if I try the same technique to test this component, i.e.:
it('renders a heading', async () => {
render(await Page());
const heading = screen.getByRole('heading', { level: 1 });
expect(heading).toBeInTheDocument();
});
^ This does’t work — I get an error that reads:
Error: Uncaught [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.]
How can we test nested server components?
I want a solution that allows me to render both the parent and descendent server component in a unit test, without mocking one or the other.
2
Answers
Your
Page
component asynchronously imports and rendersUserData
, which leads to the error you are seeing because React expects all children to be resolved before they are rendered.In your case: render a component (
<UserData />
) that is the result of an async function directly within another component’s return statement. React expects all children to be elements, strings, numbers, or arrays thereof, not Promises.To test nested server components, you need to make sure all asynchronous operations are completed before attempting to render the component tree. However, React does not natively support rendering asynchronous components directly in tests. You need to resolve these promises manually or use a testing strategy that accommodates server components.
Since you mentioned wanting to render both the parent and descendant server components without mocking, you will need to make sure your test environment can handle asynchronous components properly.
React Server Components are designed to be rendered on the server and then streamed to the client.
If you are using a tool like Next.js, which has built-in support for React Server Components, you might need to rely on end-to-end testing tools like Playwright or Cypress to fully test these components as they would be used in a production environment.
However, if you are looking for a unit test approach, and assuming you have control over the
UserData
andPage
component’s export to make them testable in isolation, you might try something like this:Use async/await to make sure all server components (and their children) are fully resolved before rendering. That might mean creating a wrapper or utility that waits for all promises within the component tree to resolve.
Since you prefer not to mock components, make sure any data fetching or external dependencies within
UserData
or any server components are resolved before testing. That could be achieved by injecting resolved data as props for testing purposes or by using a testing library that supports server components.That would look like:
That would assume a utility
renderServerComponent
that can handle the asynchronous nature of server components (which might not be supported).Consider also the question "Objects are not valid as a React child (found: [object Promise])", which is also attempting to directly use the result of an asynchronous operation (a Promise) as a React component’s child, which React does not support.
It highlights good practices like:
Using lifecycle methods (for class components) like
componentDidMount
, or hooks likeuseEffect
(for functional components) to fetch data asynchronously. Once the data is fetched, update the state of the component, which will trigger a re-render with the new data.Managing the state of the component to handle the loading, success, and error states of the asynchronous operation. That approach allows you to render different UI elements based on the current state (e.g., a loading spinner while the data is being fetched).
Rendering the components conditionally based on the fetched data. For example, only render
<UserData />
once the data it depends on has been successfully fetched and is available.In your case:
Here,
UserData
is assumed to be a component that can accept data as props. The async data fetching is handled insideuseEffect
, and the state is updated with the fetched data, which triggers a re-render ofPage
with the actual data.You would use testing utilities that allow for asynchronous operations and state changes to be awaited and observed. Tools like React Testing Library, which do provide ways to wait for elements to appear as a result of asynchronous operations completing.
Given the updated
Page
component, which fetches data asynchronously and then sets it into state, a test might look something like this:Mocking the
UserData
fetch function to return a resolved promise with mock data. That avoids making actual HTTP requests during tests.Using React Testing Library’s
render
function to render thePage
component.Using
waitFor
orfindBy
functions to wait for the asynchronous operation to complete and the component to render the fetched data.But, again, here the
UserData
function is mocked to return a resolved promise with mock data immediately, which simulates fetching data from an API without the need for actual network requests. That might not be what you want.The
waitFor
function from React Testing Library is used to wait for the component to update based on the asynchronous operation. That is key for testing components that depend on asynchronous data fetching.After waiting for the asynchronous operation to complete, assertions are made to check that the component correctly renders the fetched data. That could involve checking for specific text, elements, or attributes that indicate the presence of the expected data.
It seems like there’s not much support for react-testing-library, see here. What about using
Suspense
, have you tried that?