agent-rules-kit
Version:
Bootstrap of **Cursor** rules (`.mdc`) and mirror documentation (`.md`) for AI agent-guided projects.
386 lines (299 loc) • 9.09 kB
Markdown
---
description: Testing best practices for Next.js applications
globs: <root>/app/**/*.ts,<root>/app/**/*.tsx,<root>/src/**/*.ts,<root>/src/**/*.tsx,<root>/pages/api/**/*.ts,<root>/tests/**/*.ts,<root>/tests/**/*.tsx
alwaysApply: false
---
# Next.js Testing Guide
This guide outlines the recommended approach to testing Next.js applications in {projectPath}.
## Testing Stack
The recommended testing tools for Next.js applications:
- **Jest**: Core testing framework
- **React Testing Library**: Component testing
- **@testing-library/jest-dom**: DOM assertions
- **Mock Service Worker (MSW)**: API mocking
- **Cypress/Playwright**: End-to-end testing
- **@next/jest**: Next.js-specific Jest configuration
## Setting Up Testing Environment
Configure Jest with Next.js:
```js
// jest.config.js
const nextJest = require('@next/jest');
const createJestConfig = nextJest({
// Path to Next.js app - set to where the next.config.js is
dir: './',
});
const customJestConfig = {
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
moduleNameMapper: {
// Handle module aliases (if you use them in tsconfig or jsconfig)
'^@/components/(.*)$': '<rootDir>/components/$1',
'^@/pages/(.*)$': '<rootDir>/pages/$1',
},
testEnvironment: 'jest-environment-jsdom',
};
module.exports = createJestConfig(customJestConfig);
```
Setup file:
```js
// jest.setup.js
import '@testing-library/jest-dom';
// Any global setup goes here
```
## Testing Pages
### Testing Client-Side Rendered Pages
```jsx
// pages/index.js
import { useState } from 'react';
export default function Home() {
const [count, setCount] = useState(0);
return (
<div>
<h1>Welcome to Next.js</h1>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
// __tests__/pages/index.test.js
import { render, screen, fireEvent } from '@testing-library/react';
import Home from '@/pages/index';
describe('Home page', () => {
it('renders the heading', () => {
render(<Home />);
expect(
screen.getByRole('heading', { name: /welcome to next.js/i })
).toBeInTheDocument();
});
it('increments counter on button click', () => {
render(<Home />);
const button = screen.getByRole('button', { name: /increment/i });
expect(screen.getByText(/count: 0/i)).toBeInTheDocument();
fireEvent.click(button);
expect(screen.getByText(/count: 1/i)).toBeInTheDocument();
});
});
```
### Testing Pages with getServerSideProps
```jsx
// pages/users.js
export default function Users({ users }) {
return (
<div>
<h1>Users</h1>
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
);
}
export async function getServerSideProps() {
const res = await fetch('https://jsonplaceholder.typicode.com/users');
const users = await res.json();
return { props: { users } };
}
// __tests__/pages/users.test.js
import { render, screen } from '@testing-library/react';
import Users from '@/pages/users';
describe('Users page', () => {
it('renders users from props', () => {
const mockUsers = [
{ id: 1, name: 'John Doe' },
{ id: 2, name: 'Jane Smith' },
];
render(<Users users={mockUsers} />);
expect(
screen.getByRole('heading', { name: /users/i })
).toBeInTheDocument();
expect(screen.getByText('John Doe')).toBeInTheDocument();
expect(screen.getByText('Jane Smith')).toBeInTheDocument();
});
});
```
### Testing Pages with getStaticProps
Testing pages with getStaticProps is similar to testing with getServerSideProps, as you're testing the component with the props it would receive:
```jsx
// __tests__/pages/static-page.test.js
import { render, screen } from '@testing-library/react';
import StaticPage from '@/pages/static-page';
describe('Static page', () => {
it('renders content from props', () => {
const mockProps = {
title: 'Static Page Title',
content: 'This is static content',
};
render(<StaticPage {...mockProps} />);
expect(
screen.getByRole('heading', { name: mockProps.title })
).toBeInTheDocument();
expect(screen.getByText(mockProps.content)).toBeInTheDocument();
});
});
```
## Testing API Routes
Create API mocks and test the handler directly:
```jsx
// pages/api/hello.js
export default function handler(req, res) {
res.status(200).json({ message: 'Hello World' });
}
// __tests__/api/hello.test.js
import { createMocks } from 'node-mocks-http';
import handler from '@/pages/api/hello';
describe('/api/hello', () => {
it('returns a message', async () => {
const { req, res } = createMocks({
method: 'GET',
});
await handler(req, res);
expect(res._getStatusCode()).toBe(200);
expect(JSON.parse(res._getData())).toEqual({ message: 'Hello World' });
});
});
```
## Testing App Router Components
For the App Router, test server and client components differently:
### Testing Server Components
Server components need to be tested with a special setup:
```jsx
// __tests__/app/ServerComponent.test.jsx
import { render } from '@testing-library/react';
import ServerComponent from '@/app/ServerComponent';
// Mock fetch for server components that make requests
global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve({ data: 'mocked data' }),
})
);
describe('ServerComponent', () => {
it('renders server-fetched data', async () => {
// Render the component (which would normally be server-rendered)
const { container } = render(await ServerComponent());
// Check the rendered output
expect(container.textContent).toContain('mocked data');
});
});
```
### Testing Client Components
Client components in the App Router can be tested like regular React components:
```jsx
// app/Counter.jsx (client component)
'use client';
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
// __tests__/app/Counter.test.jsx
import { render, screen, fireEvent } from '@testing-library/react';
import Counter from '@/app/Counter';
describe('Counter', () => {
it('increments the count when clicked', () => {
render(<Counter />);
const button = screen.getByRole('button', { name: /increment/i });
expect(screen.getByText(/count: 0/i)).toBeInTheDocument();
fireEvent.click(button);
expect(screen.getByText(/count: 1/i)).toBeInTheDocument();
});
});
```
## Testing Next.js Context Providers
Many Next.js apps use context providers. Here's how to test components that use them:
```jsx
// __tests__/contexts.test.jsx
import { render, screen } from '@testing-library/react';
import { MyContext } from '@/contexts/MyContext';
import MyComponent from '@/components/MyComponent';
describe('Component with context', () => {
it('renders with context value', () => {
const contextValue = { theme: 'dark', toggleTheme: jest.fn() };
render(
<MyContext.Provider value={contextValue}>
<MyComponent />
</MyContext.Provider>
);
expect(screen.getByText(/dark theme/i)).toBeInTheDocument();
});
});
```
## Testing with Next.js Router
Test components that use the Next.js router:
```jsx
// For Pages Router
import { RouterContext } from 'next/dist/shared/lib/router-context';
const mockRouter = {
pathname: '/',
route: '/',
query: {},
asPath: '/',
push: jest.fn(),
replace: jest.fn(),
back: jest.fn(),
};
// In your test
render(
<RouterContext.Provider value={mockRouter}>
<MyComponent />
</RouterContext.Provider>
);
// For App Router
import { useRouter } from 'next/navigation';
// Mock the hook
jest.mock('next/navigation', () => ({
useRouter: jest.fn(),
}));
// Setup the mock implementation for each test
useRouter.mockImplementation(() => ({
push: jest.fn(),
replace: jest.fn(),
back: jest.fn(),
}));
```
## End-to-End Testing
Use Cypress or Playwright for E2E testing:
```js
// cypress/e2e/homepage.cy.js
describe('Homepage', () => {
it('navigates to about page when clicking the link', () => {
cy.visit('/');
cy.findByText('About').click();
cy.url().should('include', '/about');
cy.findByRole('heading', { name: /about us/i }).should('be.visible');
});
});
```
## Testing Best Practices
1. **Test behavior, not implementation**: Focus on what the user experiences.
2. **Mock API requests**: Use MSW to intercept network requests.
3. **Test common user flows**: Prioritize testing common user journeys.
4. **Separate unit and integration tests**: Maintain a distinction between unit tests for individual components and integration tests for features.
5. **Organize tests by feature**: Structure tests to mirror your app's organization.
## Code Coverage
Aim for high test coverage, especially for critical paths:
```json
// package.json
"scripts": {
"test": "jest",
"test:coverage": "jest --coverage"
}
```
Coverage thresholds can be set in jest.config.js:
```js
const customJestConfig = {
// ...other config
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
},
};
```