|
| 1 | +# Testing in Next.js API Routes |
| 2 | + |
| 3 | +This project uses Jest to test Next.js API routes. Tests are configured to run in a node environment to properly test server-side code. |
| 4 | + |
| 5 | +## Setup |
| 6 | + |
| 7 | +The testing setup includes: |
| 8 | + |
| 9 | +- Jest for the test runner |
| 10 | +- Next.js App Router API route testing |
| 11 | +- Mocking utilities for API route testing |
| 12 | + |
| 13 | +## Running Tests |
| 14 | + |
| 15 | +Run the tests using any of these commands: |
| 16 | + |
| 17 | +```bash |
| 18 | +# Run all tests |
| 19 | +npx jest |
| 20 | + |
| 21 | +# Run tests in watch mode for development |
| 22 | +npx jest --watch |
| 23 | + |
| 24 | +# Run tests with coverage report |
| 25 | +npx jest --coverage |
| 26 | + |
| 27 | +# Run a specific test file |
| 28 | +npx jest __tests__/api/health.test.ts |
| 29 | +``` |
| 30 | + |
| 31 | +## Test File Structure |
| 32 | + |
| 33 | +Test files are located in the `__tests__` directory, mirroring the structure of the `/app` directory for API routes. |
| 34 | + |
| 35 | +Example: |
| 36 | +- API route: `/app/api/health/route.ts` |
| 37 | +- Test file: `/__tests__/api/health.test.ts` |
| 38 | + |
| 39 | +## Mocking Dependencies |
| 40 | + |
| 41 | +The most common pattern for testing Next.js API routes is to mock the dependencies. Here's how to do it in your tests: |
| 42 | + |
| 43 | +```typescript |
| 44 | +// Mock modules BEFORE importing the component |
| 45 | +jest.mock('@/db/drizzle', () => ({ |
| 46 | + db: { |
| 47 | + query: { |
| 48 | + users: { |
| 49 | + findFirst: jest.fn() |
| 50 | + } |
| 51 | + } |
| 52 | + } |
| 53 | +})); |
| 54 | + |
| 55 | +// Now import the handler from the route file |
| 56 | +import { GET } from '@/app/api/your-route/route'; |
| 57 | + |
| 58 | +// Import the mocked modules to control them in tests |
| 59 | +const { db } = require('@/db/drizzle'); |
| 60 | +``` |
| 61 | + |
| 62 | +## Using Test Utilities |
| 63 | + |
| 64 | +We've created some test utilities to make testing API routes easier: |
| 65 | + |
| 66 | +```typescript |
| 67 | +import { createMockRequest, parseJsonResponse } from '../utils/api-test-utils'; |
| 68 | + |
| 69 | +// Create a mock request |
| 70 | +const req = createMockRequest('https://localhost:3000/api/health', { |
| 71 | + method: 'GET', |
| 72 | + params: { id: '123' }, |
| 73 | +}); |
| 74 | + |
| 75 | +// Execute the handler |
| 76 | +const response = await GET(req); |
| 77 | + |
| 78 | +// Parse the response |
| 79 | +const data = await parseJsonResponse(response); |
| 80 | +expect(data.status).toBe('healthy'); |
| 81 | +``` |
| 82 | + |
| 83 | +## Example Tests |
| 84 | + |
| 85 | +### Testing a GET API Route |
| 86 | + |
| 87 | +```typescript |
| 88 | +/** |
| 89 | + * @jest-environment node |
| 90 | + */ |
| 91 | +// Mock modules before importing the component |
| 92 | +jest.mock('@/db/drizzle', () => ({ |
| 93 | + db: { |
| 94 | + execute: jest.fn().mockResolvedValue({}) |
| 95 | + } |
| 96 | +})); |
| 97 | + |
| 98 | +// Import after mocks are defined |
| 99 | +import { NextRequest } from 'next/server'; |
| 100 | +import { GET } from '@/app/api/health/route'; |
| 101 | + |
| 102 | +describe('Health API Endpoint', () => { |
| 103 | + it('should return a healthy status', async () => { |
| 104 | + // Create a mock request |
| 105 | + const req = new NextRequest(new Request('https://localhost:3000/api/health')); |
| 106 | + |
| 107 | + // Execute the handler |
| 108 | + const response = await GET(req); |
| 109 | + |
| 110 | + // Check the response |
| 111 | + expect(response.status).toBe(200); |
| 112 | + const data = await response.json(); |
| 113 | + expect(data.status).toBe('healthy'); |
| 114 | + }); |
| 115 | +}); |
| 116 | +``` |
| 117 | + |
| 118 | +### Testing a POST API Route |
| 119 | + |
| 120 | +```typescript |
| 121 | +/** |
| 122 | + * @jest-environment node |
| 123 | + */ |
| 124 | +// Mock modules before importing the component |
| 125 | +jest.mock('@/db/drizzle', () => ({ |
| 126 | + db: { |
| 127 | + query: { |
| 128 | + users: { |
| 129 | + findFirst: jest.fn() |
| 130 | + } |
| 131 | + } |
| 132 | + } |
| 133 | +})); |
| 134 | + |
| 135 | +jest.mock('@/lib/password', () => ({ |
| 136 | + verifyPassword: jest.fn() |
| 137 | +})); |
| 138 | + |
| 139 | +// Import after mocks are defined |
| 140 | +import { NextRequest } from 'next/server'; |
| 141 | +import { POST } from '@/app/api/auth/login/route'; |
| 142 | + |
| 143 | +// Import mocked modules |
| 144 | +const { db } = require('@/db/drizzle'); |
| 145 | +const { verifyPassword } = require('@/lib/password'); |
| 146 | + |
| 147 | +describe('Login API Endpoint', () => { |
| 148 | + it('should successfully login a user', async () => { |
| 149 | + // Mock a user being found |
| 150 | + db.query.users.findFirst.mockResolvedValue({ |
| 151 | + id: 'user-123', |
| 152 | + |
| 153 | + }); |
| 154 | + |
| 155 | + // Mock password verification |
| 156 | + verifyPassword.mockResolvedValue(true); |
| 157 | + |
| 158 | + // Create login request |
| 159 | + const req = new NextRequest('https://localhost:3000/api/auth/login', { |
| 160 | + method: 'POST', |
| 161 | + headers: { 'Content-Type': 'application/json' }, |
| 162 | + body: JSON.stringify({ |
| 163 | + |
| 164 | + password: 'password123' |
| 165 | + }) |
| 166 | + }); |
| 167 | + |
| 168 | + // Execute the handler |
| 169 | + const response = await POST(req); |
| 170 | + |
| 171 | + // Check the response |
| 172 | + expect(response.status).toBe(200); |
| 173 | + }); |
| 174 | +}); |
| 175 | +``` |
| 176 | + |
| 177 | +## Best Practices |
| 178 | + |
| 179 | +1. **Use Node Environment**: Always use the node environment for API route tests: |
| 180 | + ```typescript |
| 181 | + /** |
| 182 | + * @jest-environment node |
| 183 | + */ |
| 184 | + ``` |
| 185 | + |
| 186 | +2. **Mock Dependencies**: Mock all external dependencies like databases, third-party APIs, etc. |
| 187 | + |
| 188 | +3. **Test Error Cases**: Test not just the happy path, but also error cases like invalid requests, not found resources, etc. |
| 189 | + |
| 190 | +4. **Clear Mocks Between Tests**: Use `beforeEach(() => { jest.clearAllMocks(); })` to reset mocks between tests. |
| 191 | + |
| 192 | +5. **Organize Tests**: Follow the structure of your application when organizing test files. |
| 193 | + |
| 194 | +## Environment Variables in Tests |
| 195 | + |
| 196 | +Environment variables needed for tests are defined in the Jest setup file. If you need to add more environment variables for testing, add them to `jest.setup.js`. |
0 commit comments