skip to Main Content

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


  1. Your Page component asynchronously imports and renders UserData, 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 and Page 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:

    // Test utility to resolve all server components before rendering
    async function renderServerComponent(componentPromise) {
      const Component = await componentPromise;
      return render(<Component />);
    }
    
    it('renders a heading and user data from server components', async () => {
      await renderServerComponent(Page());
    
      const heading = screen.getByRole('heading', { level: 1 });
      expect(heading).toBeInTheDocument();
    
      // Assuming UserData renders identifiable content
      expect(screen.getByTestId('user-data')).toBeInTheDocument();
    });
    

    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 like useEffect (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:

    import React, { useState, useEffect } from 'react';
    
    function Page() {
      const [userData, setUserData] = useState(null);
    
      useEffect(() => {
        async function fetchData() {
          const data = await UserData(); // Assuming UserData is a function that fetches data
          setUserData(data);
        }
        fetchData();
      }, []);
    
      if (!userData) {
        return <div>Loading...</div>; // Or any other loading state
      }
    
      return (
        <main className="main">
          <h1>Title</h1>
          <LogInOut />
          {userData && <UserData data={userData} />} // Passing fetched data as props (if necessary)
        </main>
      );
    }
    

    Here, UserData is assumed to be a component that can accept data as props. The async data fetching is handled inside useEffect, and the state is updated with the fetched data, which triggers a re-render of Page 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 the Page component.

    • Using waitFor or findBy functions to wait for the asynchronous operation to complete and the component to render the fetched data.

    import React from 'react';
    import { render, screen, waitFor } from '@testing-library/react';
    import Page from './Page'; // Import your component
    import UserData from './UserData'; // Import the UserData component or function
    
    // Mock the UserData function to return a resolved promise with mock data
    jest.mock('./UserData', () => {
      return jest.fn(() => Promise.resolve({ name: 'John Doe', id: '123' }));
    });
    
    describe('Page component', () => {
      it('renders the user data after fetching', async () => {
        render(<Page />);
    
        // Wait for the "Loading..." text to be removed, indicating the fetch has completed
        await waitFor(() => expect(screen.queryByText('Loading...')).not.toBeInTheDocument());
    
        // Alternatively, use findByText to wait for the specific data to appear
        // That assumes UserData renders elements that include the mocked data (e.g., a name or id)
        expect(await screen.findByText('John Doe')).toBeInTheDocument();
      });
    });
    

    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.

    Login or Signup to reply.
  2. It seems like there’s not much support for react-testing-library, see here. What about using Suspense, have you tried that?

    import { render, screen } from "@testing-library/react";
    import { Suspense } from "react";
    
    it('renders a heading', async () => {
      render(
        <Suspense>
          <Page />
        </Suspense>
      );
    
      const heading = await screen.getByRole('heading', { level: 1 });
    
      expect(heading).toBeInTheDocument();
    });
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search