6 min read
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
• 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...
• 4 min read
In today’s interconnected digital world, APIs (Application Programming Interfaces) are the backbone of communication between different software applications. From mobile apps to cloud services, APIs e...
• 5 min read
In the modern digital ecosystem, APIs (Application Programming Interfaces) serve as the backbone of connectivity. Whether you're building microservices, enabling integrations, or crafting data pipelin...