Comprehensive Unit & Integration Testing for RESTful APIs
This project demonstrates comprehensive API testing using Jest and Supertest for a Task Manager RESTful API. It covers unit tests, integration tests, database testing, error handling, and validation testing.
The Task Manager API allows users to create, read, update, and delete tasks with features like status tracking, priority levels, and due dates. All endpoints are thoroughly tested to ensure reliability and correct behavior.
Statements:
Branches:
Functions:
Lines:
Retrieve all tasks with optional filtering by status and priority
Retrieve a specific task by ID
Create a new task
Update an existing task
Delete a task by ID
Try out the Task Manager API functionality below:
Comprehensive test suite covering all API endpoints and edge cases:
// tests/tasks.test.js
const request = require('supertest');
const app = require('../app');
const Task = require('../models/Task');
const { connectDB, closeDB, clearDB } = require('./setup');
describe('Task Manager API Tests', () => {
beforeAll(async () => {
await connectDB();
});
afterAll(async () => {
await closeDB();
});
beforeEach(async () => {
await clearDB();
});
describe('POST /api/tasks', () => {
it('should create a new task with valid data', async () => {
const newTask = {
title: 'Test Task',
description: 'Test Description',
priority: 'high',
dueDate: '2025-12-31'
};
const response = await request(app)
.post('/api/tasks')
.send(newTask)
.expect('Content-Type', /json/)
.expect(201);
expect(response.body.success).toBe(true);
expect(response.body.data.title).toBe(newTask.title);
expect(response.body.data).toHaveProperty('id');
expect(response.body.data.status).toBe('pending');
});
it('should fail when title is missing', async () => {
const invalidTask = {
description: 'Missing title'
};
const response = await request(app)
.post('/api/tasks')
.send(invalidTask)
.expect(400);
expect(response.body.success).toBe(false);
expect(response.body.error).toContain('Title is required');
});
it('should validate priority values', async () => {
const invalidTask = {
title: 'Test Task',
priority: 'invalid-priority'
};
const response = await request(app)
.post('/api/tasks')
.send(invalidTask)
.expect(400);
expect(response.body.error).toMatch(/priority/i);
});
it('should set default status to pending', async () => {
const newTask = {
title: 'Test Task'
};
const response = await request(app)
.post('/api/tasks')
.send(newTask)
.expect(201);
expect(response.body.data.status).toBe('pending');
});
});
describe('GET /api/tasks', () => {
beforeEach(async () => {
// Seed test data
await Task.create([
{ title: 'Task 1', status: 'pending', priority: 'high' },
{ title: 'Task 2', status: 'completed', priority: 'low' },
{ title: 'Task 3', status: 'pending', priority: 'medium' }
]);
});
it('should retrieve all tasks', async () => {
const response = await request(app)
.get('/api/tasks')
.expect(200);
expect(response.body.success).toBe(true);
expect(response.body.data).toHaveLength(3);
});
it('should filter tasks by status', async () => {
const response = await request(app)
.get('/api/tasks?status=pending')
.expect(200);
expect(response.body.data).toHaveLength(2);
expect(response.body.data.every(task =>
task.status === 'pending'
)).toBe(true);
});
it('should filter tasks by priority', async () => {
const response = await request(app)
.get('/api/tasks?priority=high')
.expect(200);
expect(response.body.data).toHaveLength(1);
expect(response.body.data[0].priority).toBe('high');
});
it('should return empty array when no tasks exist', async () => {
await clearDB();
const response = await request(app)
.get('/api/tasks')
.expect(200);
expect(response.body.data).toHaveLength(0);
});
});
describe('GET /api/tasks/:id', () => {
let taskId;
beforeEach(async () => {
const task = await Task.create({
title: 'Test Task',
description: 'Test Description'
});
taskId = task._id;
});
it('should retrieve task by valid ID', async () => {
const response = await request(app)
.get(`/api/tasks/${taskId}`)
.expect(200);
expect(response.body.success).toBe(true);
expect(response.body.data.title).toBe('Test Task');
});
it('should return 404 for non-existent task', async () => {
const fakeId = '507f1f77bcf86cd799439011';
const response = await request(app)
.get(`/api/tasks/${fakeId}`)
.expect(404);
expect(response.body.success).toBe(false);
expect(response.body.error).toContain('not found');
});
it('should return 400 for invalid ID format', async () => {
const response = await request(app)
.get('/api/tasks/invalid-id')
.expect(400);
expect(response.body.error).toMatch(/invalid/i);
});
});
describe('PUT /api/tasks/:id', () => {
let taskId;
beforeEach(async () => {
const task = await Task.create({
title: 'Original Title',
status: 'pending'
});
taskId = task._id;
});
it('should update task with valid data', async () => {
const updates = {
title: 'Updated Title',
status: 'completed'
};
const response = await request(app)
.put(`/api/tasks/${taskId}`)
.send(updates)
.expect(200);
expect(response.body.data.title).toBe('Updated Title');
expect(response.body.data.status).toBe('completed');
});
it('should not update non-existent fields', async () => {
const response = await request(app)
.put(`/api/tasks/${taskId}`)
.send({ invalidField: 'test' })
.expect(200);
expect(response.body.data).not.toHaveProperty('invalidField');
});
it('should validate status values on update', async () => {
const response = await request(app)
.put(`/api/tasks/${taskId}`)
.send({ status: 'invalid-status' })
.expect(400);
expect(response.body.error).toMatch(/status/i);
});
});
describe('DELETE /api/tasks/:id', () => {
let taskId;
beforeEach(async () => {
const task = await Task.create({ title: 'To Delete' });
taskId = task._id;
});
it('should delete existing task', async () => {
const response = await request(app)
.delete(`/api/tasks/${taskId}`)
.expect(200);
expect(response.body.success).toBe(true);
expect(response.body.message).toContain('deleted');
// Verify task was deleted
const deletedTask = await Task.findById(taskId);
expect(deletedTask).toBeNull();
});
it('should return 404 when deleting non-existent task', async () => {
const fakeId = '507f1f77bcf86cd799439011';
await request(app)
.delete(`/api/tasks/${fakeId}`)
.expect(404);
});
});
describe('Error Handling', () => {
it('should handle database errors gracefully', async () => {
// Simulate database error
jest.spyOn(Task, 'find').mockRejectedValue(
new Error('Database connection failed')
);
const response = await request(app)
.get('/api/tasks')
.expect(500);
expect(response.body.success).toBe(false);
expect(response.body.error).toBeDefined();
});
it('should return proper error for malformed JSON', async () => {
const response = await request(app)
.post('/api/tasks')
.set('Content-Type', 'application/json')
.send('{ invalid json }')
.expect(400);
expect(response.body.error).toMatch(/json/i);
});
});
});
// jest.config.js
module.exports = {
testEnvironment: 'node',
coverageDirectory: 'coverage',
collectCoverageFrom: [
'src/**/*.js',
'!src/server.js',
'!**/node_modules/**'
],
coverageThreshold: {
global: {
branches: 90,
functions: 90,
lines: 90,
statements: 90
}
},
testMatch: ['**/tests/**/*.test.js'],
verbose: true,
setupFilesAfterEnv: ['./tests/setup.js']
};
// tests/setup.js
const mongoose = require('mongoose');
const { MongoMemoryServer } = require('mongodb-memory-server');
let mongoServer;
// Connect to test database
const connectDB = async () => {
mongoServer = await MongoMemoryServer.create();
const mongoUri = mongoServer.getUri();
await mongoose.connect(mongoUri, {
useNewUrlParser: true,
useUnifiedTopology: true
});
};
// Disconnect and close
const closeDB = async () => {
await mongoose.connection.dropDatabase();
await mongoose.connection.close();
await mongoServer.stop();
};
// Clear all collections
const clearDB = async () => {
const collections = mongoose.connection.collections;
for (const key in collections) {
await collections[key].deleteMany({});
}
};
module.exports = { connectDB, closeDB, clearDB };
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"test:verbose": "jest --verbose",
"test:single": "jest --testNamePattern"
},
"devDependencies": {
"jest": "^29.7.0",
"supertest": "^6.3.3",
"mongodb-memory-server": "^9.1.3"
}
}
// Testing promises and async/await
it('should handle async database operations', async () => {
const task = await Task.create({ title: 'Async Test' });
expect(task).toBeDefined();
expect(task.title).toBe('Async Test');
});
// Testing with done callback
it('should call callback on completion', (done) => {
request(app)
.get('/api/tasks')
.end((err, res) => {
expect(res.status).toBe(200);
done();
});
});
// Mock external email service
jest.mock('../services/emailService', () => ({
sendTaskNotification: jest.fn().mockResolvedValue(true)
}));
it('should send notification on task creation', async () => {
const emailService = require('../services/emailService');
await request(app)
.post('/api/tasks')
.send({ title: 'Important Task' });
expect(emailService.sendTaskNotification).toHaveBeenCalled();
});
// Test multiple scenarios with test.each
describe.each([
['pending', 'low', true],
['completed', 'high', true],
['invalid', 'medium', false]
])('Status validation', (status, priority, shouldPass) => {
it(`should ${shouldPass ? 'accept' : 'reject'} ${status}`, async () => {
const response = await request(app)
.post('/api/tasks')
.send({ title: 'Test', status, priority });
expect(response.status).toBe(shouldPass ? 201 : 400);
});
});
This project provided comprehensive experience with API testing best practices and Jest framework capabilities: