Introduction
Test-Driven Development is crucial for building reliable and maintainable APIs. This article explores how to effectively test API endpoints and ensure they meet their specifications.
Endpoint Testing
// userApi.test.js
import request from 'supertest';
import app from './app';
describe('User API', () => {
test('should create user', async () => {
const userData = {
name: 'John Doe',
email: 'john@example.com'
};
const response = await request(app)
.post('/api/users')
.send(userData);
expect(response.status).toBe(201);
expect(response.body).toMatchObject({
id: expect.any(Number),
...userData
});
});
test('should validate user data', async () => {
const response = await request(app)
.post('/api/users')
.send({
name: 'John',
email: 'invalid-email'
});
expect(response.status).toBe(400);
expect(response.body).toHaveProperty('errors');
});
});
Middleware Testing
// authMiddleware.test.js
describe('Auth Middleware', () => {
test('should authenticate valid token', async () => {
const token = generateValidToken();
const response = await request(app)
.get('/api/protected')
.set('Authorization', `Bearer ${token}`);
expect(response.status).toBe(200);
});
test('should reject invalid token', async () => {
const response = await request(app)
.get('/api/protected')
.set('Authorization', 'Bearer invalid-token');
expect(response.status).toBe(401);
});
test('should reject missing token', async () => {
const response = await request(app)
.get('/api/protected');
expect(response.status).toBe(401);
});
});
Error Handling
// errorHandler.test.js
describe('Error Handler', () => {
test('should handle validation errors', async () => {
const response = await request(app)
.post('/api/users')
.send({});
expect(response.status).toBe(400);
expect(response.body).toMatchObject({
error: 'Validation Error',
details: expect.any(Array)
});
});
test('should handle not found errors', async () => {
const response = await request(app)
.get('/api/users/999');
expect(response.status).toBe(404);
expect(response.body).toMatchObject({
error: 'Not Found'
});
});
test('should handle server errors', async () => {
const response = await request(app)
.get('/api/error');
expect(response.status).toBe(500);
expect(response.body).toMatchObject({
error: 'Internal Server Error'
});
});
});
Rate Limiting
// rateLimiter.test.js
describe('Rate Limiter', () => {
test('should allow requests within limit', async () => {
const requests = Array(5).fill().map(() =>
request(app).get('/api/limited')
);
const responses = await Promise.all(requests);
expect(responses.every(r => r.status === 200)).toBe(true);
});
test('should reject requests over limit', async () => {
const requests = Array(6).fill().map(() =>
request(app).get('/api/limited')
);
const responses = await Promise.all(requests);
expect(responses.some(r => r.status === 429)).toBe(true);
});
});
Best Practices
- Test all HTTP methods
- Verify response status codes
- Check response body structure
- Test error handling
- Verify security measures
Conclusion
TDD in API development ensures that endpoints are reliable, secure, and maintainable. By following these practices, you can create robust APIs that meet their specifications.
"API testing is about ensuring that your endpoints behave as expected and handle errors gracefully." - Roy Fielding
Member discussion: