
Route Tester
- 51 installs
- 101 repo stars
- Updated November 28, 2025
- blencorp/claude-code-kit
Route Tester is a Claude Code skill that provides framework-agnostic HTTP API route testing patterns, auth strategies, and integration testing best practices.
About
Route Tester is a skill that gives Claude framework-agnostic HTTP API route testing patterns, authentication strategies, and integration testing best practices. A developer uses it when writing unit, integration, or end-to-end tests for REST endpoints across Express, Next.js, FastAPI, Django REST, or Flask. It covers JWT cookie and bearer-token auth testing and per-HTTP-method assertions.
- Framework-agnostic API route testing for Express, Next.js, FastAPI, Django REST, Flask
- Auth testing for JWT cookie and bearer-token flows
- Per-method (GET/POST/PATCH/DELETE) request and response assertions
Route Tester by the numbers
- 51 all-time installs (skills.sh)
- Ranked #1,222 of 2,153 Testing & QA skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
route-tester capabilities & compatibility
- Capabilities
- testing · api development · code review
- Use cases
- testing · api development
- IDEs
- vscode · cursor ide
What route-tester says it does
Framework-agnostic HTTP API route testing patterns, authentication strategies, and integration testing best practices.
This skill provides framework-agnostic guidance for testing HTTP API routes and endpoints across any backend framework (Express, Next.js API Routes, FastAPI, Django REST, Flask, etc.).
Fast execution (< 50ms per test)
npx skills add https://github.com/blencorp/claude-code-kit --skill route-testerAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 51 |
|---|---|
| repo stars | ★ 101 |
| Last updated | November 28, 2025 |
| Repository | blencorp/claude-code-kit ↗ |
What it does
Write unit, integration, and end-to-end tests for HTTP API routes with auth flows.
Who is it for?
Writing tests for REST API routes with authentication
Skip if: Frontend UI testing or non-HTTP code
When should I use this skill?
Testing HTTP API routes and endpoints across any backend framework
What you get
API routes have unit, integration, and end-to-end tests covering auth and each HTTP method.
- Unit tests
- Integration tests
- End-to-end tests
By the numbers
- 3 test types: unit, integration, end-to-end
- unit tests under 50ms per test
Files
API Route Testing Skill
This skill provides framework-agnostic guidance for testing HTTP API routes and endpoints across any backend framework (Express, Next.js API Routes, FastAPI, Django REST, Flask, etc.).
Core Testing Principles
1. Test Types for API Routes
Unit Tests
- Test individual route handlers in isolation
- Mock dependencies (database, external APIs)
- Fast execution (< 50ms per test)
- Focus on business logic
Integration Tests
- Test full request/response cycle
- Real database (test instance)
- Authentication flow included
- Slower but more comprehensive
End-to-End Tests
- Test from client perspective
- Full authentication flow
- Real services (or close replicas)
- Most realistic, slowest execution
2. Authentication Testing Patterns
JWT Cookie Authentication
// Common pattern across frameworks
describe('Protected Route Tests', () => {
let authCookie: string;
beforeEach(async () => {
// Login and get JWT cookie
const loginResponse = await request(app)
.post('/api/auth/login')
.send({ email: 'test@example.com', password: 'password123' });
authCookie = loginResponse.headers['set-cookie'][0];
});
it('should access protected route with valid cookie', async () => {
const response = await request(app)
.get('/api/protected/resource')
.set('Cookie', authCookie);
expect(response.status).toBe(200);
});
it('should reject access without cookie', async () => {
const response = await request(app)
.get('/api/protected/resource');
expect(response.status).toBe(401);
});
});JWT Bearer Token Authentication
describe('Bearer Token Auth', () => {
let token: string;
beforeEach(async () => {
const response = await request(app)
.post('/api/auth/login')
.send({ email: 'test@example.com', password: 'password123' });
token = response.body.token;
});
it('should authenticate with bearer token', async () => {
const response = await request(app)
.get('/api/protected/resource')
.set('Authorization', `Bearer ${token}`);
expect(response.status).toBe(200);
});
});3. HTTP Method Testing
GET Requests
describe('GET /api/users', () => {
it('should return paginated users', async () => {
const response = await request(app)
.get('/api/users?page=1&limit=10');
expect(response.status).toBe(200);
expect(response.body).toHaveProperty('data');
expect(response.body).toHaveProperty('pagination');
expect(Array.isArray(response.body.data)).toBe(true);
});
it('should filter users by query params', async () => {
const response = await request(app)
.get('/api/users?role=admin');
expect(response.status).toBe(200);
expect(response.body.data.every(u => u.role === 'admin')).toBe(true);
});
});POST Requests
describe('POST /api/users', () => {
it('should create new user with valid data', async () => {
const newUser = {
name: 'John Doe',
email: 'john@example.com',
role: 'user'
};
const response = await request(app)
.post('/api/users')
.set('Cookie', authCookie)
.send(newUser);
expect(response.status).toBe(201);
expect(response.body).toMatchObject(newUser);
expect(response.body).toHaveProperty('id');
});
it('should reject invalid data', async () => {
const invalidUser = {
name: 'John Doe'
// Missing required email field
};
const response = await request(app)
.post('/api/users')
.set('Cookie', authCookie)
.send(invalidUser);
expect(response.status).toBe(400);
expect(response.body).toHaveProperty('errors');
});
});PUT/PATCH Requests
describe('PATCH /api/users/:id', () => {
it('should update user fields', async () => {
const updates = { name: 'Jane Doe' };
const response = await request(app)
.patch('/api/users/123')
.set('Cookie', authCookie)
.send(updates);
expect(response.status).toBe(200);
expect(response.body.name).toBe('Jane Doe');
});
it('should return 404 for non-existent user', async () => {
const response = await request(app)
.patch('/api/users/999999')
.set('Cookie', authCookie)
.send({ name: 'Test' });
expect(response.status).toBe(404);
});
});DELETE Requests
describe('DELETE /api/users/:id', () => {
it('should delete user and return success', async () => {
const response = await request(app)
.delete('/api/users/123')
.set('Cookie', authCookie);
expect(response.status).toBe(204);
});
it('should prevent unauthorized deletion', async () => {
const response = await request(app)
.delete('/api/users/123');
// No auth cookie
expect(response.status).toBe(401);
});
});4. Response Validation
Status Codes
describe('HTTP Status Codes', () => {
it('200 OK - Successful GET', async () => {
const response = await request(app).get('/api/users');
expect(response.status).toBe(200);
});
it('201 Created - Successful POST', async () => {
const response = await request(app).post('/api/users').send(validData);
expect(response.status).toBe(201);
});
it('204 No Content - Successful DELETE', async () => {
const response = await request(app).delete('/api/users/123');
expect(response.status).toBe(204);
});
it('400 Bad Request - Invalid input', async () => {
const response = await request(app).post('/api/users').send({});
expect(response.status).toBe(400);
});
it('401 Unauthorized - Missing auth', async () => {
const response = await request(app).get('/api/protected');
expect(response.status).toBe(401);
});
it('403 Forbidden - Insufficient permissions', async () => {
const response = await request(app).delete('/api/admin/users/123').set('Cookie', userCookie);
expect(response.status).toBe(403);
});
it('404 Not Found - Non-existent resource', async () => {
const response = await request(app).get('/api/users/999999');
expect(response.status).toBe(404);
});
it('500 Internal Server Error - Server failure', async () => {
// Test error handling
mockDatabase.findOne.mockRejectedValue(new Error('DB Error'));
const response = await request(app).get('/api/users/123');
expect(response.status).toBe(500);
});
});Response Schema Validation
describe('Response Schema', () => {
it('should match expected schema', async () => {
const response = await request(app).get('/api/users/123');
expect(response.body).toEqual({
id: expect.any(String),
name: expect.any(String),
email: expect.any(String),
role: expect.stringMatching(/^(user|admin)$/),
createdAt: expect.any(String),
updatedAt: expect.any(String)
});
});
});5. Error Handling Tests
describe('Error Handling', () => {
it('should return structured error response', async () => {
const response = await request(app)
.post('/api/users')
.send({ invalid: 'data' });
expect(response.status).toBe(400);
expect(response.body).toEqual({
error: expect.any(String),
message: expect.any(String),
errors: expect.any(Array)
});
});
it('should handle database errors gracefully', async () => {
mockDatabase.findOne.mockRejectedValue(new Error('Connection lost'));
const response = await request(app).get('/api/users/123');
expect(response.status).toBe(500);
expect(response.body.error).toBe('Internal Server Error');
});
it('should sanitize error messages in production', async () => {
process.env.NODE_ENV = 'production';
const response = await request(app).get('/api/error-prone-route');
expect(response.status).toBe(500);
expect(response.body.message).not.toContain('stack trace');
expect(response.body.message).not.toContain('SQL');
});
});6. Test Setup and Teardown
describe('API Tests', () => {
let testDatabase;
beforeAll(async () => {
// Initialize test database
testDatabase = await initTestDatabase();
});
afterAll(async () => {
// Clean up test database
await testDatabase.close();
});
beforeEach(async () => {
// Seed test data
await testDatabase.seed();
});
afterEach(async () => {
// Clear test data
await testDatabase.clear();
});
// Tests...
});Framework-Specific Testing Libraries
While this skill provides framework-agnostic patterns, here are common testing libraries per framework:
- Express: supertest, jest, vitest
- Next.js API Routes: @testing-library/react, next-test-api-route-handler
- FastAPI: pytest, httpx
- Django REST: django.test.TestCase, rest_framework.test
- Flask: pytest, flask.testing
Best Practices
1. Use descriptive test names - Test names should describe the scenario and expected outcome 2. Test happy path and edge cases - Cover both success and failure scenarios 3. Isolate tests - Each test should be independent and not rely on other tests 4. Use realistic test data - Test data should mimic production data 5. Clean up after tests - Always reset state between tests 6. Mock external dependencies - Don't call real external APIs in tests 7. Test authentication edge cases - Expired tokens, invalid tokens, missing tokens 8. Validate response schemas - Ensure APIs return expected structure 9. Test rate limiting - Verify rate limits work correctly 10. Test CORS headers - Ensure CORS is configured correctly
Common Pitfalls
❌ Don't share state between tests
// Bad
let userId;
it('creates user', async () => {
const response = await request(app).post('/api/users').send(userData);
userId = response.body.id; // Shared state!
});
it('deletes user', async () => {
await request(app).delete(`/api/users/${userId}`); // Depends on previous test
});✅ Do create fresh state for each test
// Good
it('creates user', async () => {
const response = await request(app).post('/api/users').send(userData);
expect(response.status).toBe(201);
});
it('deletes user', async () => {
const user = await createTestUser();
const response = await request(app).delete(`/api/users/${user.id}`);
expect(response.status).toBe(204);
});Additional Resources
See the resources/ directory for more detailed guides:
http-testing-fundamentals.md- Deep dive into HTTP testing conceptsauthentication-testing.md- Authentication strategies and edge casesapi-integration-testing.md- Integration testing patterns and tools
Quick Reference
Test Structure
describe('Resource Name', () => {
describe('HTTP Method /path', () => {
it('should describe expected behavior', async () => {
// Arrange
const testData = {...};
// Act
const response = await request(app)
.method('/path')
.set('Cookie', authCookie)
.send(testData);
// Assert
expect(response.status).toBe(expectedStatus);
expect(response.body).toMatchObject(expectedData);
});
});
});Authentication Pattern
let authCookie: string;
beforeEach(async () => {
const response = await request(app)
.post('/api/auth/login')
.send({ email: 'test@example.com', password: 'password123' });
authCookie = response.headers['set-cookie'][0];
});
// Use authCookie in protected route tests
.set('Cookie', authCookie)API Integration Testing
Integration Testing Strategy
Integration tests verify that multiple components work together correctly. For APIs, this means testing the full request-response cycle including routing, middleware, business logic, and database operations.
Test Environment Setup
Database Setup
import { PrismaClient } from '@prisma/client';
let prisma: PrismaClient;
beforeAll(async () => {
// Use test database
prisma = new PrismaClient({
datasources: {
db: {
url: process.env.TEST_DATABASE_URL
}
}
});
// Run migrations
await prisma.$executeRawUnsafe('CREATE DATABASE IF NOT EXISTS test_db');
// execSync('npm prisma migrate deploy');
});
afterAll(async () => {
await prisma.$disconnect();
});
beforeEach(async () => {
// Seed test data
await seedTestData(prisma);
});
afterEach(async () => {
// Clean up test data
await prisma.user.deleteMany();
await prisma.post.deleteMany();
// ... clear other tables
});Application Initialization
import { createApp } from '../src/app';
let app;
let server;
beforeAll(async () => {
// Initialize application
app = await createApp({
database: testDatabaseConfig,
redis: testRedisConfig,
// ... other test config
});
// Start server on random port
server = app.listen(0);
});
afterAll(async () => {
// Cleanup
await server.close();
await app.cleanup();
});Full Flow Integration Tests
CRUD Operation Flow
describe('User CRUD Integration', () => {
let authCookie: string;
let createdUserId: string;
beforeEach(async () => {
// Setup: Login as admin
const loginResponse = await request(app)
.post('/api/auth/login')
.send({ email: 'admin@example.com', password: 'admin123' });
authCookie = loginResponse.headers['set-cookie'][0];
});
it('should complete full CRUD cycle', async () => {
// CREATE
const createResponse = await request(app)
.post('/api/users')
.set('Cookie', authCookie)
.send({
name: 'John Doe',
email: 'john@example.com',
role: 'user'
});
expect(createResponse.status).toBe(201);
expect(createResponse.body).toHaveProperty('id');
createdUserId = createResponse.body.id;
// READ - Get single user
const readResponse = await request(app)
.get(`/api/users/${createdUserId}`)
.set('Cookie', authCookie);
expect(readResponse.status).toBe(200);
expect(readResponse.body.email).toBe('john@example.com');
// READ - List users
const listResponse = await request(app)
.get('/api/users')
.set('Cookie', authCookie);
expect(listResponse.status).toBe(200);
expect(listResponse.body.data).toContainEqual(
expect.objectContaining({ id: createdUserId })
);
// UPDATE
const updateResponse = await request(app)
.patch(`/api/users/${createdUserId}`)
.set('Cookie', authCookie)
.send({ name: 'Jane Doe' });
expect(updateResponse.status).toBe(200);
expect(updateResponse.body.name).toBe('Jane Doe');
// Verify update persisted
const verifyResponse = await request(app)
.get(`/api/users/${createdUserId}`)
.set('Cookie', authCookie);
expect(verifyResponse.body.name).toBe('Jane Doe');
// DELETE
const deleteResponse = await request(app)
.delete(`/api/users/${createdUserId}`)
.set('Cookie', authCookie);
expect(deleteResponse.status).toBe(204);
// Verify deletion
const afterDeleteResponse = await request(app)
.get(`/api/users/${createdUserId}`)
.set('Cookie', authCookie);
expect(afterDeleteResponse.status).toBe(404);
});
});Multi-Step Business Process
describe('E-commerce Order Flow', () => {
let customerCookie: string;
let productId: string;
let cartId: string;
let orderId: string;
beforeEach(async () => {
// Setup: Create customer and product
const customer = await createTestUser({ role: 'customer' });
customerCookie = await loginAs(customer.email, 'password123');
const product = await createTestProduct({
name: 'Test Product',
price: 29.99,
stock: 10
});
productId = product.id;
});
it('should complete full order flow', async () => {
// Step 1: Add item to cart
const addToCartResponse = await request(app)
.post('/api/cart/items')
.set('Cookie', customerCookie)
.send({ productId, quantity: 2 });
expect(addToCartResponse.status).toBe(201);
cartId = addToCartResponse.body.cartId;
// Step 2: View cart
const viewCartResponse = await request(app)
.get('/api/cart')
.set('Cookie', customerCookie);
expect(viewCartResponse.status).toBe(200);
expect(viewCartResponse.body.items).toHaveLength(1);
expect(viewCartResponse.body.total).toBe(59.98); // 2 * 29.99
// Step 3: Checkout
const checkoutResponse = await request(app)
.post('/api/checkout')
.set('Cookie', customerCookie)
.send({
shippingAddress: {
street: '123 Main St',
city: 'Anytown',
country: 'US',
postalCode: '12345'
},
paymentMethod: 'credit_card'
});
expect(checkoutResponse.status).toBe(201);
orderId = checkoutResponse.body.orderId;
// Step 4: Verify order created
const orderResponse = await request(app)
.get(`/api/orders/${orderId}`)
.set('Cookie', customerCookie);
expect(orderResponse.status).toBe(200);
expect(orderResponse.body.status).toBe('pending');
expect(orderResponse.body.items).toHaveLength(1);
// Step 5: Verify stock reduced
const productResponse = await request(app)
.get(`/api/products/${productId}`);
expect(productResponse.body.stock).toBe(8); // 10 - 2
// Step 6: Verify cart cleared
const cartAfterCheckout = await request(app)
.get('/api/cart')
.set('Cookie', customerCookie);
expect(cartAfterCheckout.body.items).toHaveLength(0);
});
it('should prevent checkout with insufficient stock', async () => {
// Add more than available stock
await request(app)
.post('/api/cart/items')
.set('Cookie', customerCookie)
.send({ productId, quantity: 20 }); // Stock is only 10
const checkoutResponse = await request(app)
.post('/api/checkout')
.set('Cookie', customerCookie)
.send({
shippingAddress: { /* ... */ },
paymentMethod: 'credit_card'
});
expect(checkoutResponse.status).toBe(400);
expect(checkoutResponse.body.error).toMatch(/insufficient stock/i);
});
});Database Integration
Transaction Testing
describe('Transaction Handling', () => {
it('should rollback on error', async () => {
const initialUserCount = await prisma.user.count();
// Attempt operation that should fail
const response = await request(app)
.post('/api/users/batch')
.send({
users: [
{ name: 'User 1', email: 'user1@example.com' },
{ name: 'User 2', email: 'invalid-email' }, // Will fail validation
{ name: 'User 3', email: 'user3@example.com' }
]
});
expect(response.status).toBe(400);
// Verify rollback - no users should be created
const finalUserCount = await prisma.user.count();
expect(finalUserCount).toBe(initialUserCount);
});
it('should commit successful transaction', async () => {
const initialUserCount = await prisma.user.count();
const response = await request(app)
.post('/api/users/batch')
.send({
users: [
{ name: 'User 1', email: 'user1@example.com' },
{ name: 'User 2', email: 'user2@example.com' },
{ name: 'User 3', email: 'user3@example.com' }
]
});
expect(response.status).toBe(201);
// Verify all users created
const finalUserCount = await prisma.user.count();
expect(finalUserCount).toBe(initialUserCount + 3);
});
});Concurrent Access
describe('Concurrent Operations', () => {
it('should handle concurrent updates without race conditions', async () => {
const user = await createTestUser({ balance: 100 });
// Simulate 10 concurrent withdrawals of $10 each
const withdrawals = Array(10).fill(null).map(() =>
request(app)
.post('/api/wallet/withdraw')
.set('Cookie', await loginAs(user.email, 'password123'))
.send({ amount: 10 })
);
const responses = await Promise.all(withdrawals);
// Verify final balance (should handle optimistic locking)
const finalUser = await prisma.user.findUnique({
where: { id: user.id }
});
expect(finalUser.balance).toBe(0);
// Verify correct number of successes and failures
const successes = responses.filter(r => r.status === 200);
const failures = responses.filter(r => r.status === 409); // Conflict
expect(successes.length).toBe(10);
expect(failures.length).toBe(0);
});
});External Service Integration
Mocking External APIs
import nock from 'nock';
describe('Payment Processing Integration', () => {
beforeEach(() => {
// Mock external payment API
nock('https://api.payment-provider.com')
.post('/v1/charges')
.reply(200, {
id: 'ch_test_123',
status: 'succeeded',
amount: 2999
});
});
afterEach(() => {
nock.cleanAll();
});
it('should process payment and create order', async () => {
const response = await request(app)
.post('/api/checkout')
.set('Cookie', customerCookie)
.send({
amount: 29.99,
paymentMethod: 'credit_card',
cardToken: 'tok_test_visa'
});
expect(response.status).toBe(201);
expect(response.body.payment.status).toBe('succeeded');
// Verify order created in database
const order = await prisma.order.findUnique({
where: { id: response.body.orderId }
});
expect(order.paymentStatus).toBe('paid');
});
it('should handle payment failure gracefully', async () => {
// Mock payment failure
nock.cleanAll();
nock('https://api.payment-provider.com')
.post('/v1/charges')
.reply(402, {
error: 'insufficient_funds'
});
const response = await request(app)
.post('/api/checkout')
.set('Cookie', customerCookie)
.send({
amount: 29.99,
paymentMethod: 'credit_card',
cardToken: 'tok_test_visa'
});
expect(response.status).toBe(402);
// Verify no order created
const orders = await prisma.order.findMany({
where: { userId: customerId }
});
expect(orders).toHaveLength(0);
});
});Middleware Integration
Authentication Middleware
describe('Authentication Middleware', () => {
it('should protect routes requiring authentication', async () => {
const protectedRoutes = [
'/api/profile',
'/api/orders',
'/api/cart',
'/api/settings'
];
for (const route of protectedRoutes) {
const response = await request(app).get(route);
expect(response.status).toBe(401);
}
});
it('should allow authenticated access', async () => {
const authCookie = await loginAs('user@example.com', 'password123');
const protectedRoutes = [
'/api/profile',
'/api/orders',
'/api/cart',
'/api/settings'
];
for (const route of protectedRoutes) {
const response = await request(app)
.get(route)
.set('Cookie', authCookie);
expect(response.status).not.toBe(401);
}
});
});Rate Limiting
describe('Rate Limiting Middleware', () => {
it('should rate limit requests', async () => {
const endpoint = '/api/search';
// Make requests up to limit
const requests = Array(100).fill(null).map(() =>
request(app).get(endpoint)
);
const responses = await Promise.all(requests);
// Some should be rate limited
const rateLimited = responses.filter(r => r.status === 429);
expect(rateLimited.length).toBeGreaterThan(0);
});
it('should reset rate limit after window', async () => {
const endpoint = '/api/search';
// Hit rate limit
await Promise.all(
Array(100).fill(null).map(() => request(app).get(endpoint))
);
// Wait for rate limit window to reset
await new Promise(resolve => setTimeout(resolve, 60000)); // 1 minute
// Should work again
const response = await request(app).get(endpoint);
expect(response.status).not.toBe(429);
});
});Error Handling Integration
describe('Global Error Handler', () => {
it('should handle validation errors', async () => {
const response = await request(app)
.post('/api/users')
.send({ invalid: 'data' });
expect(response.status).toBe(400);
expect(response.body).toHaveProperty('error');
expect(response.body).toHaveProperty('errors');
});
it('should handle database errors', async () => {
// Trigger unique constraint violation
await createTestUser({ email: 'duplicate@example.com' });
const response = await request(app)
.post('/api/users')
.send({
name: 'Test User',
email: 'duplicate@example.com'
});
expect(response.status).toBe(409); // Conflict
expect(response.body.error).toMatch(/already exists/i);
});
it('should handle internal errors safely in production', async () => {
process.env.NODE_ENV = 'production';
// Trigger internal error
const response = await request(app).get('/api/error-prone-route');
expect(response.status).toBe(500);
expect(response.body.error).toBe('Internal Server Error');
expect(response.body).not.toHaveProperty('stack');
expect(response.body.message).not.toContain('database');
});
});Performance Testing
describe('API Performance', () => {
it('should respond within acceptable time', async () => {
const start = Date.now();
const response = await request(app).get('/api/users?page=1&limit=50');
const duration = Date.now() - start;
expect(response.status).toBe(200);
expect(duration).toBeLessThan(500); // 500ms
});
it('should handle N+1 queries efficiently', async () => {
// Create test data
await createTestUsers(100);
await createTestPosts(500); // 5 posts per user
const start = Date.now();
const response = await request(app).get('/api/users?include=posts');
const duration = Date.now() - start;
expect(response.status).toBe(200);
expect(response.body.data).toHaveLength(100);
expect(duration).toBeLessThan(1000); // Should use eager loading
});
});Test Helpers
// Database seeding
async function seedTestData(prisma: PrismaClient) {
await prisma.user.createMany({
data: [
{ email: 'admin@example.com', password: hashPassword('admin123'), role: 'admin' },
{ email: 'user@example.com', password: hashPassword('user123'), role: 'user' },
{ email: 'moderator@example.com', password: hashPassword('mod123'), role: 'moderator' }
]
});
await prisma.product.createMany({
data: [
{ name: 'Product 1', price: 19.99, stock: 100 },
{ name: 'Product 2', price: 29.99, stock: 50 },
{ name: 'Product 3', price: 39.99, stock: 25 }
]
});
}
// Test data creation
async function createTestUser(data: Partial<User>) {
return await prisma.user.create({
data: {
name: 'Test User',
email: `test-${Date.now()}@example.com`,
password: hashPassword('password123'),
role: 'user',
...data
}
});
}
async function createTestProduct(data: Partial<Product>) {
return await prisma.product.create({
data: {
name: 'Test Product',
price: 29.99,
stock: 10,
...data
}
});
}
// Authentication helper
async function loginAs(email: string, password: string): Promise<string> {
const response = await request(app)
.post('/api/auth/login')
.send({ email, password });
return response.headers['set-cookie'][0];
}Best Practices
✅ Do:
- Use isolated test database
- Seed fresh test data for each test
- Clean up test data after each test
- Mock external services
- Test full request-response cycle
- Test database transactions
- Test concurrent operations
- Test middleware integration
- Verify side effects (database changes, emails sent, etc.)
- Test performance and timeouts
❌ Don't:
- Use production database for tests
- Share state between tests
- Make real external API calls
- Skip cleanup (causes flaky tests)
- Ignore race conditions
- Hard-code test data across tests
- Test only happy paths
- Forget to test error handling
Authentication Testing
Authentication Patterns
1. JWT Cookie Authentication
The most common pattern for web applications - JWT stored in HTTP-only cookies.
Basic Setup
describe('JWT Cookie Authentication', () => {
let authCookie: string;
let userCookie: string;
let adminCookie: string;
beforeEach(async () => {
// Admin user
const adminResponse = await request(app)
.post('/api/auth/login')
.send({
email: 'admin@example.com',
password: 'admin123'
});
adminCookie = adminResponse.headers['set-cookie'][0];
// Regular user
const userResponse = await request(app)
.post('/api/auth/login')
.send({
email: 'user@example.com',
password: 'user123'
});
userCookie = userResponse.headers['set-cookie'][0];
});
it('should set HTTP-only cookie on login', async () => {
const response = await request(app)
.post('/api/auth/login')
.send({ email: 'test@example.com', password: 'password123' });
expect(response.status).toBe(200);
expect(response.headers['set-cookie']).toBeDefined();
const cookie = response.headers['set-cookie'][0];
expect(cookie).toContain('HttpOnly');
expect(cookie).toContain('Secure'); // Should be present in production
expect(cookie).toContain('SameSite');
});
it('should access protected route with valid cookie', async () => {
const response = await request(app)
.get('/api/protected/profile')
.set('Cookie', userCookie);
expect(response.status).toBe(200);
expect(response.body).toHaveProperty('user');
});
it('should reject access without cookie', async () => {
const response = await request(app)
.get('/api/protected/profile');
expect(response.status).toBe(401);
expect(response.body.error).toMatch(/unauthorized|authentication required/i);
});
});Cookie Security Attributes
describe('Cookie Security', () => {
it('should set Secure flag in production', async () => {
process.env.NODE_ENV = 'production';
const response = await request(app)
.post('/api/auth/login')
.send({ email: 'test@example.com', password: 'password123' });
const cookie = response.headers['set-cookie'][0];
expect(cookie).toContain('Secure');
});
it('should set SameSite attribute', async () => {
const response = await request(app)
.post('/api/auth/login')
.send({ email: 'test@example.com', password: 'password123' });
const cookie = response.headers['set-cookie'][0];
expect(cookie).toMatch(/SameSite=(Strict|Lax)/);
});
it('should set appropriate expiration', async () => {
const response = await request(app)
.post('/api/auth/login')
.send({ email: 'test@example.com', password: 'password123' });
const cookie = response.headers['set-cookie'][0];
expect(cookie).toContain('Max-Age=');
// Or: expect(cookie).toContain('Expires=');
});
});2. JWT Bearer Token Authentication
Common for mobile apps and API-to-API communication.
describe('Bearer Token Authentication', () => {
let token: string;
let adminToken: string;
beforeEach(async () => {
// Get user token
const userResponse = await request(app)
.post('/api/auth/login')
.send({ email: 'user@example.com', password: 'password123' });
token = userResponse.body.token;
// Get admin token
const adminResponse = await request(app)
.post('/api/auth/login')
.send({ email: 'admin@example.com', password: 'admin123' });
adminToken = adminResponse.body.token;
});
it('should return token on successful login', async () => {
const response = await request(app)
.post('/api/auth/login')
.send({ email: 'test@example.com', password: 'password123' });
expect(response.status).toBe(200);
expect(response.body).toHaveProperty('token');
expect(response.body.token).toMatch(/^[\w-]+\.[\w-]+\.[\w-]+$/); // JWT format
});
it('should authenticate with Bearer token', async () => {
const response = await request(app)
.get('/api/protected/profile')
.set('Authorization', `Bearer ${token}`);
expect(response.status).toBe(200);
});
it('should reject missing Authorization header', async () => {
const response = await request(app)
.get('/api/protected/profile');
expect(response.status).toBe(401);
});
it('should reject malformed Authorization header', async () => {
const response = await request(app)
.get('/api/protected/profile')
.set('Authorization', 'InvalidFormat');
expect(response.status).toBe(401);
});
it('should reject invalid token', async () => {
const response = await request(app)
.get('/api/protected/profile')
.set('Authorization', 'Bearer invalid.token.here');
expect(response.status).toBe(401);
});
});3. API Key Authentication
Simple authentication for server-to-server communication.
describe('API Key Authentication', () => {
const validApiKey = 'sk_test_abc123xyz';
it('should authenticate with valid API key', async () => {
const response = await request(app)
.get('/api/data')
.set('X-API-Key', validApiKey);
expect(response.status).toBe(200);
});
it('should reject missing API key', async () => {
const response = await request(app).get('/api/data');
expect(response.status).toBe(401);
});
it('should reject invalid API key', async () => {
const response = await request(app)
.get('/api/data')
.set('X-API-Key', 'invalid-key');
expect(response.status).toBe(401);
});
it('should rate limit by API key', async () => {
const requests = Array(101).fill(null).map(() =>
request(app).get('/api/data').set('X-API-Key', validApiKey)
);
const responses = await Promise.all(requests);
const rateLimited = responses.filter(r => r.status === 429);
expect(rateLimited.length).toBeGreaterThan(0);
});
});Authorization (Role-Based Access Control)
Testing Permission Levels
describe('Role-Based Authorization', () => {
let userCookie: string;
let adminCookie: string;
let moderatorCookie: string;
beforeEach(async () => {
userCookie = await loginAs('user@example.com', 'password');
adminCookie = await loginAs('admin@example.com', 'password');
moderatorCookie = await loginAs('moderator@example.com', 'password');
});
describe('User Permissions', () => {
it('should allow user to read own profile', async () => {
const response = await request(app)
.get('/api/profile')
.set('Cookie', userCookie);
expect(response.status).toBe(200);
});
it('should allow user to update own profile', async () => {
const response = await request(app)
.patch('/api/profile')
.set('Cookie', userCookie)
.send({ name: 'Updated Name' });
expect(response.status).toBe(200);
});
it('should forbid user from reading other profiles', async () => {
const response = await request(app)
.get('/api/users/other-user-id')
.set('Cookie', userCookie);
expect(response.status).toBe(403);
});
it('should forbid user from accessing admin routes', async () => {
const response = await request(app)
.get('/api/admin/users')
.set('Cookie', userCookie);
expect(response.status).toBe(403);
});
});
describe('Moderator Permissions', () => {
it('should allow moderator to read user profiles', async () => {
const response = await request(app)
.get('/api/users/any-user-id')
.set('Cookie', moderatorCookie);
expect(response.status).toBe(200);
});
it('should allow moderator to update user content', async () => {
const response = await request(app)
.patch('/api/posts/123')
.set('Cookie', moderatorCookie)
.send({ status: 'approved' });
expect(response.status).toBe(200);
});
it('should forbid moderator from deleting users', async () => {
const response = await request(app)
.delete('/api/users/123')
.set('Cookie', moderatorCookie);
expect(response.status).toBe(403);
});
});
describe('Admin Permissions', () => {
it('should allow admin full access', async () => {
const endpoints = [
'/api/admin/users',
'/api/admin/settings',
'/api/users/123',
'/api/admin/stats'
];
for (const endpoint of endpoints) {
const response = await request(app)
.get(endpoint)
.set('Cookie', adminCookie);
expect(response.status).not.toBe(403);
}
});
it('should allow admin to delete users', async () => {
const response = await request(app)
.delete('/api/users/123')
.set('Cookie', adminCookie);
expect(response.status).toBe(204);
});
});
});Token Expiration and Refresh
describe('Token Expiration', () => {
it('should reject expired token', async () => {
// Create token with short expiration
const shortLivedToken = createTestToken({ expiresIn: '1ms' });
// Wait for expiration
await new Promise(resolve => setTimeout(resolve, 10));
const response = await request(app)
.get('/api/protected/profile')
.set('Authorization', `Bearer ${shortLivedToken}`);
expect(response.status).toBe(401);
expect(response.body.error).toMatch(/expired|invalid/i);
});
it('should refresh token with valid refresh token', async () => {
const loginResponse = await request(app)
.post('/api/auth/login')
.send({ email: 'test@example.com', password: 'password123' });
const refreshToken = loginResponse.body.refreshToken;
const refreshResponse = await request(app)
.post('/api/auth/refresh')
.send({ refreshToken });
expect(refreshResponse.status).toBe(200);
expect(refreshResponse.body).toHaveProperty('token');
expect(refreshResponse.body.token).not.toBe(loginResponse.body.token);
});
it('should reject invalid refresh token', async () => {
const response = await request(app)
.post('/api/auth/refresh')
.send({ refreshToken: 'invalid-refresh-token' });
expect(response.status).toBe(401);
});
it('should invalidate refresh token after use (one-time use)', async () => {
const loginResponse = await request(app)
.post('/api/auth/login')
.send({ email: 'test@example.com', password: 'password123' });
const refreshToken = loginResponse.body.refreshToken;
// First refresh - should work
const firstRefresh = await request(app)
.post('/api/auth/refresh')
.send({ refreshToken });
expect(firstRefresh.status).toBe(200);
// Second refresh with same token - should fail
const secondRefresh = await request(app)
.post('/api/auth/refresh')
.send({ refreshToken });
expect(secondRefresh.status).toBe(401);
});
});Login/Logout Flow Testing
describe('Authentication Flow', () => {
it('should complete full login-use-logout cycle', async () => {
// Login
const loginResponse = await request(app)
.post('/api/auth/login')
.send({ email: 'test@example.com', password: 'password123' });
expect(loginResponse.status).toBe(200);
const authCookie = loginResponse.headers['set-cookie'][0];
// Use authenticated endpoint
const profileResponse = await request(app)
.get('/api/profile')
.set('Cookie', authCookie);
expect(profileResponse.status).toBe(200);
// Logout
const logoutResponse = await request(app)
.post('/api/auth/logout')
.set('Cookie', authCookie);
expect(logoutResponse.status).toBe(200);
// Verify token is invalidated
const afterLogoutResponse = await request(app)
.get('/api/profile')
.set('Cookie', authCookie);
expect(afterLogoutResponse.status).toBe(401);
});
it('should reject invalid credentials', async () => {
const response = await request(app)
.post('/api/auth/login')
.send({ email: 'test@example.com', password: 'wrong-password' });
expect(response.status).toBe(401);
expect(response.body.error).toMatch(/invalid credentials|unauthorized/i);
});
it('should handle non-existent user', async () => {
const response = await request(app)
.post('/api/auth/login')
.send({ email: 'nonexistent@example.com', password: 'password123' });
expect(response.status).toBe(401);
// Should NOT reveal that user doesn't exist (security)
expect(response.body.error).toMatch(/invalid credentials/i);
});
});Session Management
describe('Session Management', () => {
it('should create session on login', async () => {
const response = await request(app)
.post('/api/auth/login')
.send({ email: 'test@example.com', password: 'password123' });
expect(response.status).toBe(200);
expect(response.headers['set-cookie']).toBeDefined();
});
it('should maintain session across requests', async () => {
const loginResponse = await request(app)
.post('/api/auth/login')
.send({ email: 'test@example.com', password: 'password123' });
const cookie = loginResponse.headers['set-cookie'][0];
// Multiple requests with same session
const request1 = await request(app).get('/api/profile').set('Cookie', cookie);
const request2 = await request(app).get('/api/data').set('Cookie', cookie);
const request3 = await request(app).get('/api/settings').set('Cookie', cookie);
expect(request1.status).toBe(200);
expect(request2.status).toBe(200);
expect(request3.status).toBe(200);
});
it('should destroy session on logout', async () => {
const loginResponse = await request(app)
.post('/api/auth/login')
.send({ email: 'test@example.com', password: 'password123' });
const cookie = loginResponse.headers['set-cookie'][0];
await request(app).post('/api/auth/logout').set('Cookie', cookie);
const response = await request(app).get('/api/profile').set('Cookie', cookie);
expect(response.status).toBe(401);
});
});Security Testing
describe('Security', () => {
it('should prevent timing attacks on login', async () => {
// Login attempts should take similar time regardless of whether user exists
const validUserStart = Date.now();
await request(app).post('/api/auth/login')
.send({ email: 'exists@example.com', password: 'wrong' });
const validUserTime = Date.now() - validUserStart;
const invalidUserStart = Date.now();
await request(app).post('/api/auth/login')
.send({ email: 'nonexistent@example.com', password: 'wrong' });
const invalidUserTime = Date.now() - invalidUserStart;
// Times should be within 100ms of each other
expect(Math.abs(validUserTime - invalidUserTime)).toBeLessThan(100);
});
it('should rate limit login attempts', async () => {
const attempts = Array(10).fill(null).map(() =>
request(app).post('/api/auth/login')
.send({ email: 'test@example.com', password: 'wrong' })
);
const responses = await Promise.all(attempts);
const rateLimited = responses.some(r => r.status === 429);
expect(rateLimited).toBe(true);
});
it('should not leak information about user existence', async () => {
const existingUserResponse = await request(app)
.post('/api/auth/login')
.send({ email: 'exists@example.com', password: 'wrong' });
const nonExistentUserResponse = await request(app)
.post('/api/auth/login')
.send({ email: 'nonexistent@example.com', password: 'wrong' });
// Error messages should be identical
expect(existingUserResponse.body.error).toBe(nonExistentUserResponse.body.error);
});
});Helper Functions
// Reusable auth helpers
async function loginAs(email: string, password: string): Promise<string> {
const response = await request(app)
.post('/api/auth/login')
.send({ email, password });
return response.headers['set-cookie'][0];
}
function createTestToken(options: { expiresIn?: string } = {}): string {
// Create JWT for testing
return jwt.sign(
{ userId: 'test-user-id', role: 'user' },
process.env.JWT_SECRET || 'test-secret',
{ expiresIn: options.expiresIn || '1h' }
);
}
async function createAuthenticatedUser(role: string = 'user') {
const user = await createTestUser({ role });
const cookie = await loginAs(user.email, 'password123');
return { user, cookie };
}Best Practices
✅ Do:
- Test both authentication (who are you?) and authorization (what can you do?)
- Test token/session expiration
- Test logout properly clears sessions
- Test rate limiting on auth endpoints
- Verify security attributes on cookies (HttpOnly, Secure, SameSite)
- Test role-based access control thoroughly
- Use timing-safe comparisons to prevent timing attacks
❌ Don't:
- Hard-code credentials in tests (use environment variables or test fixtures)
- Leak information about user existence in error messages
- Skip testing edge cases (expired tokens, malformed headers, etc.)
- Share authentication state between tests
- Use production auth services in tests
- Store passwords in plain text (even in tests)
HTTP Testing Fundamentals
Understanding HTTP Testing
HTTP testing validates that your API endpoints behave correctly when receiving requests and returning responses. This includes testing request handling, response formats, status codes, headers, and error conditions.
Core HTTP Concepts for Testing
1. HTTP Methods (Verbs)
GET - Retrieve resources
- Should be idempotent (multiple identical requests produce same result)
- No request body
- Safe (doesn't modify server state)
POST - Create new resources
- Has request body
- Not idempotent (multiple requests create multiple resources)
- Returns 201 Created with resource location
PUT - Replace entire resource
- Has request body
- Idempotent (multiple identical requests produce same result)
- Returns 200 OK or 204 No Content
PATCH - Partial update of resource
- Has request body with only fields to update
- May or may not be idempotent (depends on implementation)
- Returns 200 OK with updated resource
DELETE - Remove resource
- Usually no request body
- Idempotent (deleting same resource multiple times has same effect)
- Returns 204 No Content or 200 OK
OPTIONS - Discover allowed methods
- Used for CORS preflight requests
- Returns allowed HTTP methods in Allow header
HEAD - Same as GET but without response body
- Used to check if resource exists or get metadata
- Returns same headers as GET would
2. HTTP Status Codes
2xx Success
200 OK- Request succeeded (GET, PUT, PATCH)201 Created- Resource created (POST)202 Accepted- Request accepted for processing (async operations)204 No Content- Success but no response body (DELETE, PUT)
3xx Redirection
301 Moved Permanently- Resource permanently moved302 Found- Temporary redirect304 Not Modified- Cached version is still valid
4xx Client Errors
400 Bad Request- Invalid request syntax or validation failed401 Unauthorized- Authentication required or failed403 Forbidden- Authenticated but not authorized404 Not Found- Resource doesn't exist405 Method Not Allowed- HTTP method not supported for this endpoint409 Conflict- Request conflicts with current state (e.g., duplicate email)422 Unprocessable Entity- Syntactically correct but semantically invalid429 Too Many Requests- Rate limit exceeded
5xx Server Errors
500 Internal Server Error- Generic server error502 Bad Gateway- Invalid response from upstream server503 Service Unavailable- Server temporarily unavailable504 Gateway Timeout- Upstream server timeout
3. HTTP Headers
Request Headers to Test
{
'Content-Type': 'application/json', // Request body format
'Accept': 'application/json', // Expected response format
'Authorization': 'Bearer <token>', // Authentication
'Cookie': 'session=abc123', // Session cookies
'User-Agent': 'MyApp/1.0', // Client identifier
'X-Request-ID': 'uuid', // Request tracking
'If-None-Match': 'etag-value', // Conditional requests
'If-Modified-Since': 'date' // Caching
}Response Headers to Validate
{
'Content-Type': 'application/json', // Response format
'Set-Cookie': 'session=xyz; HttpOnly; Secure', // Set cookies
'Cache-Control': 'no-cache', // Caching policy
'ETag': '"resource-version"', // Resource version
'Location': '/api/users/123', // Created resource URL (201)
'X-RateLimit-Remaining': '99', // Rate limit info
'Access-Control-Allow-Origin': '*', // CORS
'Content-Length': '1234' // Response size
}Testing Patterns
Test Structure (AAA Pattern)
it('should create user with valid data', async () => {
// Arrange - Set up test data and preconditions
const newUser = {
name: 'John Doe',
email: 'john@example.com',
role: 'user'
};
// Act - Execute the operation being tested
const response = await request(app)
.post('/api/users')
.send(newUser);
// Assert - Verify the results
expect(response.status).toBe(201);
expect(response.body).toMatchObject(newUser);
expect(response.body.id).toBeDefined();
});Happy Path vs Edge Cases
Happy Path - Normal, expected flow
it('should return user by id', async () => {
const user = await createTestUser();
const response = await request(app).get(`/api/users/${user.id}`);
expect(response.status).toBe(200);
expect(response.body.id).toBe(user.id);
});Edge Cases - Unusual or error conditions
describe('Edge Cases', () => {
it('should return 404 for non-existent user', async () => {
const response = await request(app).get('/api/users/999999');
expect(response.status).toBe(404);
});
it('should return 400 for invalid id format', async () => {
const response = await request(app).get('/api/users/invalid-id');
expect(response.status).toBe(400);
});
it('should handle special characters in query params', async () => {
const response = await request(app).get('/api/users?name=O%27Brien');
expect(response.status).toBe(200);
});
it('should reject extremely large request body', async () => {
const hugeData = { data: 'x'.repeat(10 * 1024 * 1024) }; // 10MB
const response = await request(app).post('/api/data').send(hugeData);
expect(response.status).toBe(413); // Payload Too Large
});
});Idempotency Testing
describe('Idempotency', () => {
it('GET should be idempotent', async () => {
const response1 = await request(app).get('/api/users/123');
const response2 = await request(app).get('/api/users/123');
const response3 = await request(app).get('/api/users/123');
expect(response1.body).toEqual(response2.body);
expect(response2.body).toEqual(response3.body);
});
it('PUT should be idempotent', async () => {
const updates = { name: 'Jane Doe' };
const response1 = await request(app).put('/api/users/123').send(updates);
const response2 = await request(app).put('/api/users/123').send(updates);
expect(response1.status).toBe(200);
expect(response2.status).toBe(200);
expect(response1.body).toEqual(response2.body);
});
it('DELETE should be idempotent', async () => {
const response1 = await request(app).delete('/api/users/123');
const response2 = await request(app).delete('/api/users/123');
expect(response1.status).toBe(204);
expect(response2.status).toBe(404); // Already deleted
});
it('POST should NOT be idempotent', async () => {
const userData = { name: 'John', email: 'john@example.com' };
const response1 = await request(app).post('/api/users').send(userData);
const response2 = await request(app).post('/api/users').send(userData);
expect(response1.status).toBe(201);
expect(response2.status).toBe(409); // Conflict - email already exists
});
});Request/Response Body Testing
JSON Request Bodies
describe('JSON Request Bodies', () => {
it('should accept valid JSON', async () => {
const response = await request(app)
.post('/api/users')
.set('Content-Type', 'application/json')
.send({ name: 'John Doe', email: 'john@example.com' });
expect(response.status).toBe(201);
});
it('should reject malformed JSON', async () => {
const response = await request(app)
.post('/api/users')
.set('Content-Type', 'application/json')
.send('{ invalid json }');
expect(response.status).toBe(400);
});
it('should reject missing required fields', async () => {
const response = await request(app)
.post('/api/users')
.send({ name: 'John Doe' }); // Missing email
expect(response.status).toBe(400);
expect(response.body.errors).toContainEqual(
expect.objectContaining({ field: 'email', message: expect.any(String) })
);
});
it('should reject invalid field types', async () => {
const response = await request(app)
.post('/api/users')
.send({ name: 123, email: 'john@example.com' }); // name should be string
expect(response.status).toBe(400);
});
});Response Body Validation
describe('Response Body Validation', () => {
it('should return expected schema', async () => {
const response = await request(app).get('/api/users/123');
expect(response.body).toEqual({
id: expect.any(String),
name: expect.any(String),
email: expect.stringMatching(/^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/),
role: expect.stringMatching(/^(user|admin|moderator)$/),
createdAt: expect.stringMatching(/^\d{4}-\d{2}-\d{2}T/), // ISO 8601
updatedAt: expect.stringMatching(/^\d{4}-\d{2}-\d{2}T/)
});
});
it('should not leak sensitive fields', async () => {
const response = await request(app).get('/api/users/123');
expect(response.body).not.toHaveProperty('password');
expect(response.body).not.toHaveProperty('passwordHash');
expect(response.body).not.toHaveProperty('resetToken');
});
it('should return paginated list format', async () => {
const response = await request(app).get('/api/users');
expect(response.body).toEqual({
data: expect.arrayContaining([
expect.objectContaining({ id: expect.any(String) })
]),
pagination: {
page: expect.any(Number),
limit: expect.any(Number),
total: expect.any(Number),
pages: expect.any(Number)
}
});
});
});Content Negotiation
Testing Different Content Types
describe('Content Negotiation', () => {
it('should return JSON by default', async () => {
const response = await request(app).get('/api/users/123');
expect(response.headers['content-type']).toContain('application/json');
expect(response.body).toBeInstanceOf(Object);
});
it('should support Accept header', async () => {
const response = await request(app)
.get('/api/users/123')
.set('Accept', 'application/json');
expect(response.status).toBe(200);
expect(response.headers['content-type']).toContain('application/json');
});
it('should return 406 for unsupported content type', async () => {
const response = await request(app)
.get('/api/users/123')
.set('Accept', 'application/xml');
expect(response.status).toBe(406); // Not Acceptable
});
});Query Parameters and URL Encoding
describe('Query Parameters', () => {
it('should handle simple query params', async () => {
const response = await request(app).get('/api/users?role=admin');
expect(response.status).toBe(200);
expect(response.body.data.every(u => u.role === 'admin')).toBe(true);
});
it('should handle multiple query params', async () => {
const response = await request(app)
.get('/api/users?role=admin&active=true&page=1');
expect(response.status).toBe(200);
});
it('should handle URL-encoded characters', async () => {
const response = await request(app)
.get('/api/users?name=O%27Brien'); // O'Brien encoded
expect(response.status).toBe(200);
});
it('should handle array query params', async () => {
const response = await request(app)
.get('/api/users?roles[]=admin&roles[]=moderator');
expect(response.status).toBe(200);
});
it('should validate query param types', async () => {
const response = await request(app).get('/api/users?page=invalid');
expect(response.status).toBe(400);
expect(response.body.errors).toContainEqual(
expect.objectContaining({ field: 'page', message: expect.any(String) })
);
});
});Performance and Timeout Testing
describe('Performance', () => {
it('should respond within acceptable time', async () => {
const start = Date.now();
const response = await request(app).get('/api/users');
const duration = Date.now() - start;
expect(response.status).toBe(200);
expect(duration).toBeLessThan(1000); // 1 second
});
it('should handle slow queries with timeout', async () => {
const response = await request(app)
.get('/api/expensive-query')
.timeout(5000); // 5 second timeout
expect(response.status).toBe(200);
});
});Best Practices Summary
✅ Do:
- Test both happy path and edge cases
- Validate response schemas
- Test idempotency for appropriate methods
- Verify proper status codes
- Test error handling
- Use realistic test data
- Clean up after each test
- Test query parameters and URL encoding
❌ Don't:
- Share state between tests
- Hard-code test data across tests
- Skip error case testing
- Ignore status codes
- Test implementation details
- Make real external API calls
- Leave test data in production databases
{
"route-tester": {
"type": "domain",
"enforcement": "suggest",
"priority": "medium",
"promptTriggers": {
"keywords": [
"test route",
"test routes",
"test api",
"test endpoint",
"test endpoints",
"api testing",
"route testing",
"endpoint testing",
"test authentication",
"test auth",
"integration test",
"integration testing",
"api test",
"http test",
"rest test",
"test request",
"test response"
],
"intentPatterns": [
"test.*(route|endpoint|api)",
"(route|endpoint|api).*test",
"(test|testing).*(authentication|auth)",
"(integration|e2e).*(test|testing).*(api|route|endpoint)",
"write.*(test|tests).*(api|route|endpoint)"
]
}
}
}
Related skills
FAQ
Is this tied to one framework?
No, it is framework-agnostic and applies to Express, Next.js API Routes, FastAPI, Django REST, and Flask.
What auth patterns are covered?
JWT cookie authentication and JWT bearer-token authentication.