# Server Components Testing Testing patterns for React 19 Server Components, Suspense, and async rendering. ## Testing Async Server Components ### Basic Async Component ```typescript // src/components/AsyncUserProfile.tsx export async function AsyncUserProfile({ userId }: { userId: string }) { // Fetch data in Server Component const response = await fetch(`/api/users/${userId}`); const user = await response.json(); return (

{user.name}

{user.email}

); } ``` ### Test Suite ```typescript // src/components/AsyncUserProfile.test.tsx import { describe, it, expect, beforeAll, afterEach, afterAll } from 'vitest'; import { render, screen } from '@testing-library/react'; import { setupServer } from 'msw/node'; import { http, HttpResponse } from 'msw'; import { AsyncUserProfile } from './AsyncUserProfile'; const server = setupServer( http.get('/api/users/:userId', ({ params }) => { return HttpResponse.json({ id: params.userId, name: 'Alice Johnson', email: 'alice@example.com', }); }) ); beforeAll(() => server.listen()); afterEach(() => server.resetHandlers()); afterAll(() => server.close()); describe('AsyncUserProfile', () => { it('renders user data after loading', async () => { // Render async component const component = await AsyncUserProfile({ userId: '123' }); render(component); expect(screen.getByText('Alice Johnson')).toBeInTheDocument(); expect(screen.getByText('alice@example.com')).toBeInTheDocument(); }); }); ``` ## Testing with Suspense Boundaries ### Component with Suspense ```typescript // src/app/users/[userId]/page.tsx import { Suspense } from 'react'; import { AsyncUserProfile } from '@/components/AsyncUserProfile'; export default function UserProfilePage({ params }: { params: { userId: string } }) { return ( Loading user profile...}> ); } ``` ### Test Suite ```typescript // src/app/users/[userId]/page.test.tsx import { describe, it, expect } from 'vitest'; import { render, screen } from '@testing-library/react'; import UserProfilePage from './page'; describe('UserProfilePage', () => { it('shows loading state initially', () => { render(); expect(screen.getByText('Loading user profile...')).toBeInTheDocument(); }); it('renders user profile after loading', async () => { render(); // Wait for async component to resolve expect(await screen.findByText('Alice Johnson')).toBeInTheDocument(); }); }); ``` ## Testing Streaming Rendering ### Streaming Component ```typescript // src/components/StreamingUserList.tsx export async function StreamingUserList() { const response = await fetch('/api/users', { // Enable streaming next: { revalidate: 60 }, }); const users = await response.json(); return ( ); } ``` ### Test Suite ```typescript // src/components/StreamingUserList.test.tsx import { describe, it, expect } from 'vitest'; import { render, screen } from '@testing-library/react'; import { StreamingUserList } from './StreamingUserList'; describe('StreamingUserList', () => { it('progressively renders user cards', async () => { const component = await StreamingUserList(); render(component); // All users should eventually appear expect(await screen.findByText('Alice Johnson')).toBeInTheDocument(); expect(await screen.findByText('Bob Smith')).toBeInTheDocument(); }); it('shows loading placeholders while streaming', () => { render(Loading list...}>); expect(screen.getByText('Loading list...')).toBeInTheDocument(); }); }); ``` ## Testing Server Actions ### Server Action ```typescript // src/actions/createUser.ts 'use server'; import { revalidatePath } from 'next/cache'; export async function createUser(formData: FormData) { const name = formData.get('name') as string; const email = formData.get('email') as string; const response = await fetch('/api/users', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name, email }), }); if (!response.ok) { throw new Error('Failed to create user'); } const user = await response.json(); // Revalidate users list revalidatePath('/users'); return user; } ``` ### Test Suite ```typescript // src/actions/createUser.test.ts import { describe, it, expect, vi } from 'vitest'; import { setupServer } from 'msw/node'; import { http, HttpResponse } from 'msw'; import { createUser } from './createUser'; // Mock revalidatePath vi.mock('next/cache', () => ({ revalidatePath: vi.fn(), })); const server = setupServer( http.post('/api/users', async ({ request }) => { const body = await request.json(); return HttpResponse.json( { id: '123', ...body }, { status: 201 } ); }) ); beforeAll(() => server.listen()); afterEach(() => server.resetHandlers()); afterAll(() => server.close()); describe('createUser', () => { it('creates user via server action', async () => { const formData = new FormData(); formData.append('name', 'Charlie'); formData.append('email', 'charlie@example.com'); const user = await createUser(formData); expect(user).toEqual({ id: '123', name: 'Charlie', email: 'charlie@example.com', }); }); it('revalidates path after creation', async () => { const { revalidatePath } = await import('next/cache'); const formData = new FormData(); formData.append('name', 'Diana'); formData.append('email', 'diana@example.com'); await createUser(formData); expect(revalidatePath).toHaveBeenCalledWith('/users'); }); }); ``` ## Testing Form Actions ### Form with Server Action ```typescript // src/components/UserForm.tsx import { createUser } from '@/actions/createUser'; export function UserForm() { return (
); } ``` ### Test Suite ```typescript // src/components/UserForm.test.tsx import { describe, it, expect, vi } from 'vitest'; import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { UserForm } from './UserForm'; // Mock the server action vi.mock('@/actions/createUser', () => ({ createUser: vi.fn(), })); describe('UserForm', () => { it('submits form with server action', async () => { const { createUser } = await import('@/actions/createUser'); const user = userEvent.setup(); render(); await user.type(screen.getByLabelText('Name'), 'Alice'); await user.type(screen.getByLabelText('Email'), 'alice@example.com'); await user.click(screen.getByRole('button', { name: 'Create User' })); expect(createUser).toHaveBeenCalledWith(expect.any(FormData)); const formData = (createUser as any).mock.calls[0][0] as FormData; expect(formData.get('name')).toBe('Alice'); expect(formData.get('email')).toBe('alice@example.com'); }); }); ``` ## Testing Error Boundaries ### Error Boundary Component ```typescript // src/components/AsyncUserError.tsx export async function AsyncUserError({ userId }: { userId: string }) { const response = await fetch(`/api/users/${userId}`); if (!response.ok) { throw new Error(`User ${userId} not found`); } const user = await response.json(); return
{user.name}
; } // src/app/users/[userId]/error.tsx 'use client'; export default function Error({ error, reset, }: { error: Error; reset: () => void; }) { return (

Something went wrong!

{error.message}

); } ``` ### Test Suite ```typescript // src/app/users/[userId]/error.test.tsx import { describe, it, expect, vi } from 'vitest'; import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import Error from './error'; describe('Error Boundary', () => { it('displays error message', () => { const error = new Error('User not found'); const reset = vi.fn(); render(); expect(screen.getByRole('alert')).toHaveTextContent('User not found'); }); it('calls reset on try again', async () => { const user = userEvent.setup(); const error = new Error('User not found'); const reset = vi.fn(); render(); await user.click(screen.getByRole('button', { name: 'Try again' })); expect(reset).toHaveBeenCalled(); }); }); ``` ## Testing Parallel Data Fetching ### Parallel Async Components ```typescript // src/app/dashboard/page.tsx import { Suspense } from 'react'; async function UserStats() { const response = await fetch('/api/stats/users'); const stats = await response.json(); return
{stats.count} users
; } async function PostStats() { const response = await fetch('/api/stats/posts'); const stats = await response.json(); return
{stats.count} posts
; } export default function Dashboard() { return (
Loading user stats...
}> Loading post stats...}> ); } ``` ### Test Suite ```typescript // src/app/dashboard/page.test.tsx import { describe, it, expect } from 'vitest'; import { render, screen } from '@testing-library/react'; import Dashboard from './page'; describe('Dashboard', () => { it('loads components in parallel', async () => { render(); // Both should load independently expect(await screen.findByText('150 users')).toBeInTheDocument(); expect(await screen.findByText('300 posts')).toBeInTheDocument(); }); it('shows individual loading states', () => { render(); expect(screen.getByText('Loading user stats...')).toBeInTheDocument(); expect(screen.getByText('Loading post stats...')).toBeInTheDocument(); }); }); ``` ## Key Takeaways 1. **Async Components**: Await component render before passing to `render()` 2. **Suspense**: Test both fallback and resolved states 3. **Server Actions**: Mock server actions and verify FormData 4. **Error Boundaries**: Test error display and reset functionality 5. **Parallel Fetching**: Each Suspense boundary loads independently --- **Next**: [Common Patterns](common-patterns.md) | **Previous**: [Best Practices](testing-best-practices.md)