dd## Setting Up Automated Testing for Your Node.js API
This tutorial builds on our documented API by implementing comprehensive automated testing. We'll cover unit tests, integration tests, and end-to-end testing using popular testing frameworks and best practices.
Prerequisites
- Completed Parts 1-5 of the tutorial
- Basic understanding of testing concepts
- Node.js and npm installed
Project Setup
Install the required dependencies:
npm install --save-dev jest supertest mongodb-memory-server faker @types/jest
npm install --save-dev @babel/core @babel/preset-env
Updated Project Structure
Add these new files to your project:
books-api/
├── __tests__/
│ ├── unit/
│ │ ├── models/
│ │ │ ├── book.test.js
│ │ │ └── user.test.js
│ │ └── utils/
│ │ └── validation.test.js
│ ├── integration/
│ │ ├── auth.test.js
│ │ └── books.test.js
│ ├── e2e/
│ │ └── api.test.js
│ └── fixtures/
│ ├── books.js
│ └── users.js
├── jest.config.js
├── babel.config.js
└── src/
├── test/
│ ├── setup.js
│ └── teardown.js
└── utils/
└── testUtils.js
Jest Configuration
Create jest.config.js
:
module.exports = {
testEnvironment: 'node',
testMatch: [
'**/__tests__/**/*.test.js',
'**/?(*.)+(spec|test).js'
],
coveragePathIgnorePatterns: [
'/node_modules/',
'/__tests__/fixtures/'
],
setupFilesAfterEnv: ['<rootDir>/src/test/setup.js'],
globalTeardown: '<rootDir>/src/test/teardown.js',
collectCoverage: true,
coverageReporters: ['text', 'lcov', 'clover'],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
}
}
};
Babel Configuration
Create babel.config.js
:
module.exports = {
presets: [
[
'@babel/preset-env',
{
targets: {
node: 'current'
}
}
]
]
};
Test Setup and Teardown
Create src/test/setup.js
:
const { MongoMemoryServer } = require('mongodb-memory-server');
const mongoose = require('mongoose');
const jwt = require('jsonwebtoken');
let mongod;
beforeAll(async () => {
// Start in-memory MongoDB instance
mongod = await MongoMemoryServer.create();
const uri = mongod.getUri();
await mongoose.connect(uri);
// Set up test environment variables
process.env.JWT_SECRET = 'test-secret';
process.env.JWT_EXPIRE = '1h';
});
afterAll(async () => {
// Cleanup
await mongoose.disconnect();
await mongod.stop();
});
beforeEach(async () => {
// Clear all collections before each test
const collections = await mongoose.connection.db.collections();
for (let collection of collections) {
await collection.deleteMany({});
}
});
// Global test helpers
global.generateAuthToken = (userId, role = 'user') => {
return jwt.sign(
{ id: userId, role },
process.env.JWT_SECRET,
{ expiresIn: process.env.JWT_EXPIRE }
);
};
Create src/test/teardown.js
:
module.exports = async () => {
// Add any cleanup code needed after all tests complete
};
Test Utilities
Create src/utils/testUtils.js
:
const faker = require('faker');
const { Book } = require('../models/book');
const { User } = require('../models/user');
const generateFakeBook = (overrides = {}) => ({
title: faker.commerce.productName(),
author: faker.name.findName(),
isbn: faker.random.alphaNumeric(13),
publishedDate: faker.date.past(),
genre: faker.random.arrayElement(['fiction', 'non-fiction', 'science', 'history']),
description: faker.lorem.paragraph(),
...overrides
});
const generateFakeUser = (overrides = {}) => ({
name: faker.name.findName(),
email: faker.internet.email(),
password: faker.internet.password(),
role: faker.random.arrayElement(['user', 'editor', 'admin']),
...overrides
});
const createTestBook = async (overrides = {}) => {
const bookData = generateFakeBook(overrides);
return await Book.create(bookData);
};
const createTestUser = async (overrides = {}) => {
const userData = generateFakeUser(overrides);
return await User.create(userData);
};
module.exports = {
generateFakeBook,
generateFakeUser,
createTestBook,
createTestUser
};
Unit Tests
Create __tests__/unit/models/book.test.js
:
const mongoose = require('mongoose');
const { Book } = require('../../../src/models/book');
const { generateFakeBook } = require('../../../src/utils/testUtils');
describe('Book Model Tests', () => {
it('should create a valid book', async () => {
const bookData = generateFakeBook();
const validBook = new Book(bookData);
const savedBook = await validBook.save();
expect(savedBook._id).toBeDefined();
expect(savedBook.title).toBe(bookData.title);
expect(savedBook.author).toBe(bookData.author);
});
it('should fail validation without required fields', async () => {
const invalidBook = new Book({});
let error;
try {
await invalidBook.save();
} catch (err) {
error = err;
}
expect(error).toBeDefined();
expect(error.errors.title).toBeDefined();
expect(error.errors.author).toBeDefined();
});
it('should sanitize HTML in description', async () => {
const bookWithHtml = generateFakeBook({
description: '<script>alert("xss")</script><p>Safe content</p>'
});
const book = new Book(bookWithHtml);
const savedBook = await book.save();
expect(savedBook.description).toBe('Safe content');
});
});
Integration Tests
Create __tests__/integration/books.test.js
:
const request = require('supertest');
const app = require('../../src/server');
const { createTestUser, createTestBook } = require('../../src/utils/testUtils');
describe('Books API Integration Tests', () => {
let authToken;
let testUser;
beforeEach(async () => {
testUser = await createTestUser({ role: 'admin' });
authToken = generateAuthToken(testUser._id, testUser.role);
});
describe('GET /api/books', () => {
it('should return all books', async () => {
// Create test books
const books = await Promise.all([
createTestBook(),
createTestBook(),
createTestBook()
]);
const response = await request(app)
.get('/api/books')
.expect(200);
expect(response.body).toHaveLength(3);
expect(response.body[0]).toHaveProperty('title');
expect(response.body[0]).toHaveProperty('author');
});
it('should filter books by genre', async () => {
await Promise.all([
createTestBook({ genre: 'fiction' }),
createTestBook({ genre: 'non-fiction' }),
createTestBook({ genre: 'fiction' })
]);
const response = await request(app)
.get('/api/books')
.query({ genre: 'fiction' })
.expect(200);
expect(response.body).toHaveLength(2);
expect(response.body.every(book => book.genre === 'fiction')).toBe(true);
});
});
describe('POST /api/books', () => {
it('should create a new book with valid auth', async () => {
const newBook = generateFakeBook();
const response = await request(app)
.post('/api/books')
.set('Authorization', `Bearer ${authToken}`)
.send(newBook)
.expect(201);
expect(response.body).toHaveProperty('_id');
expect(response.body.title).toBe(newBook.title);
});
it('should reject unauthorized creation attempts', async () => {
const newBook = generateFakeBook();
await request(app)
.post('/api/books')
.send(newBook)
.expect(401);
});
});
});
End-to-End Tests
Create __tests__/e2e/api.test.js
:
const request = require('supertest');
const app = require('../../src/server');
const { createTestUser, generateFakeBook } = require('../../src/utils/testUtils');
describe('API End-to-End Tests', () => {
let authToken;
let testUser;
beforeAll(async () => {
// Set up test user and authentication
testUser = await createTestUser({ role: 'admin' });
const loginResponse = await request(app)
.post('/api/auth/login')
.send({
email: testUser.email,
password: testUser.password
});
authToken = loginResponse.body.token;
});
describe('Complete Book Management Flow', () => {
it('should perform full CRUD operation sequence', async () => {
// Create a new book
const newBook = generateFakeBook();
const createResponse = await request(app)
.post('/api/books')
.set('Authorization', `Bearer ${authToken}`)
.send(newBook)
.expect(201);
const bookId = createResponse.body._id;
// Read the created book
const getResponse = await request(app)
.get(`/api/books/${bookId}`)
.expect(200);
expect(getResponse.body.title).toBe(newBook.title);
// Update the book
const updateData = { title: 'Updated Title' };
await request(app)
.put(`/api/books/${bookId}`)
.set('Authorization', `Bearer ${authToken}`)
.send(updateData)
.expect(200);
// Verify update
const updatedResponse = await request(app)
.get(`/api/books/${bookId}`)
.expect(200);
expect(updatedResponse.body.title).toBe(updateData.title);
// Delete the book
await request(app)
.delete(`/api/books/${bookId}`)
.set('Authorization', `Bearer ${authToken}`)
.expect(200);
// Verify deletion
await request(app)
.get(`/api/books/${bookId}`)
.expect(404);
});
});
});
Test Reports and Coverage
Add scripts to package.json
:
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"test:unit": "jest --testMatch='**/__tests__/unit/**/*.test.js'",
"test:integration": "jest --testMatch='**/__tests__/integration/**/*.test.js'",
"test:e2e": "jest --testMatch='**/__tests__/e2e/**/*.test.js'",
"test:ci": "jest --ci --coverage --reporters='default' --reporters='jest-junit'"
}
}
GitHub Actions Integration
Create .github/workflows/test.yml
:
name: Test
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [14.x, 16.x]
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm run test:ci
- name: Upload coverage
uses: codecov/codecov-action@v2
with:
file: ./coverage/lcov.info
Performance Testing
Create __tests__/performance/load.test.js
:
const autocannon = require('autocannon');
const performLoadTest = async () => {
const result = await autocannon({
url: 'http://localhost:3000/api/books',
connections: 10,
duration: 10,
pipelining: 1,
headers: {
'Accept': 'application/json'
}
});
console.log(result);
};
performLoadTest();
Best Practices Implemented
Test Organization
- Clear separation of test types
- Reusable test utilities
- Consistent naming conventions
- Modular test structure
Test Coverage
- Unit tests for models and utilities
- Integration tests for API endpoints
- End-to-end tests for complete flows
- Performance testing
Test Environment
- In-memory MongoDB for tests
- Environment variable management
- Clean state between tests
- Proper async/await handling
CI/CD Integration
- GitHub Actions configuration
- Coverage reporting
- Multiple Node.js versions
- Automated test runs
Next Steps
To enhance your testing setup:
- Add API contract testing
- Implement mutation testing
- Add visual regression testing
- Set up load testing
- Add security testing
- Implement browser testing
- Add accessibility testing
Conclusion
You now have a comprehensive testing suite that:
- Ensures code quality
- Catches bugs early
- Maintains API reliability
- Supports continuous integration
- Provides confidence in changes
Remember to:
- Keep tests up to date
- Monitor test coverage
- Review test performance
- Update test data regularly
- Document test procedures
The next tutorial will cover adding pagination for GET requests.
Related Posts
6 min read
The Model Context Protocol (MCP) has emerged as one of the most significant developments in AI technology in 2025. Launched by Anthropic in November 2024, MCP is an open standard designed to bridge AI...
5 min read
APIs (Application Programming Interfaces) are the backbone of modern digital applications. They allow different software systems to communicate, exchange data, and collaborate seamlessly. As businesse...