Integration testing is crucial in ensuring that different modules and components of an application work together as expected. In this guide, we will cover:
- Testing interactions between modules and components
- Mocking dependencies and external services
- Testing database interactions
We'll use Jest as our testing framework and Supertest for HTTP assertions.
Setting Up the Project
First, we'll set up our Node.js project and install the necessary dependencies.
Create a new directory for the project and initialize it:
mkdir integration-testing
cd integration-testing
npm init -y
Install the required dependencies:
npm install --save express mongoose
npm install --save-dev jest supertest
Update the package.json to add a test script:
"scripts": {
"test": "jest"
}
Creating the Application
Defining the Application Structure
We'll create a simple Express application with MongoDB as the database. Our application will have two main components: users and posts.
Create the following directory structure for your project:
integration-testing/
├── src/
│ ├── app.js
│ ├── models/
│ │ └── user.js
│ ├── routes/
│ │ └── user.js
│ └── services/
│ └── user.js
└── tests/
└── user.test.js
Creating the Express Application
To do that, create src/app.js
and add the code snippet below:
const express = require('express');
const mongoose = require('mongoose');
const userRouter = require('./routes/user');
const app = express();
// Connect to the MongoDB database
mongoose.connect('mongodb://localhost:27017/integration_testing', {
useNewUrlParser: true,
useUnifiedTopology: true,
});
// Middleware to parse JSON requests
app.use(express.json());
// Use the user router for any requests to /users
app.use('/users', userRouter);
module.exports = app;
In this file, we set up an Express application, connect to a MongoDB database, and define a route for user-related operations.
Defining the User Model
Next, create src/models/user.js
:
const mongoose = require('mongoose');
// Define the user schema with name and email fields
const userSchema = new mongoose.Schema({
name: { type: String, required: true },
email: { type: String, required: true, unique: true },
});
// Create the User model from the schema
const User = mongoose.model('User', userSchema);
module.exports = User;
This file defines a Mongoose schema and model for users, with name and email fields.
Creating the User Service
Create src/services/user.js
:
const User = require('../models/user');
// Function to create a new user
async function createUser(name, email) {
const user = new User({ name, email });
await user.save();
return user;
}
// Function to get a user by email
async function getUserByEmail(email) {
return User.findOne({ email });
}
module.exports = { createUser, getUserByEmail };
This file contains functions that interact with the user model to create a new user and retrieve a user by email.
Creating the User Routes
Create src/routes/user.js
:
const express = require('express');
const { createUser, getUserByEmail } = require('../services/user');
const router = express.Router();
// Route to create a new user
router.post('/', async (req, res) => {
const { name, email } = req.body;
try {
const user = await createUser(name, email);
res.status(201).json(user);
} catch (error) {
res.status(400).json({ error: error.message });
}
});
// Route to get a user by email
router.get('/:email', async (req, res) => {
const { email } = req.params;
try {
const user = await getUserByEmail(email);
if (user) {
res.status(200).json(user);
} else {
res.status(404).json({ error: 'User not found' });
}
} catch (error) {
res.status(400).json({ error: error.message });
}
});
module.exports = router;
This file defines the routes for creating and retrieving users using the user service functions.
Integration Testing
Testing Interactions Between Modules and Components
We'll test the interaction between our Express routes, services, and MongoDB.
Create tests/user.test.js
:
const mongoose = require('mongoose');
const request = require('supertest');
const app = require('../src/app');
const User = require('../src/models/user');
// Connect to the MongoDB database before all tests
beforeAll(async () => {
const url = `mongodb://127.0.0.1/integration_testing`;
await mongoose.connect(url, { useNewUrlParser: true, useUnifiedTopology: true });
});
// Clean up the database and close the connection after all tests
afterAll(async () => {
await mongoose.connection.db.dropDatabase();
await mongoose.connection.close();
});
describe('User API', () => {
// Test creating a new user
it('should create a user', async () => {
const response = await request(app).post('/users').send({
name: 'John Doe',
email: 'john@example.com',
});
expect(response.status).toBe(201);
expect(response.body.name).toBe('John Doe');
expect(response.body.email).toBe('john@example.com');
});
// Test retrieving a user by email
it('should get a user by email', async () => {
const user = new User({ name: 'Jane Doe', email: 'jane@example.com' });
await user.save();
const response = await request(app).get(`/users/jane@example.com`);
expect(response.status).toBe(200);
expect(response.body.name).toBe('Jane Doe');
expect(response.body.email).toBe('jane@example.com');
});
// Test returning 404 if user not found
it('should return 404 if user not found', async () => {
const response = await request(app).get('/users/nonexistent@example.com');
expect(response.status).toBe(404);
});
});
In this test file, we set up a connection to a MongoDB database, clean up the database after each test, and define tests for creating and retrieving users.
Mocking Dependencies and External Services
Sometimes, you might need to mock dependencies or external services to isolate the tested component. We'll mock the User model to simulate database interactions.
Update tests/user.test.js
to mock the User model:
const request = require('supertest');
const app = require('../src/app');
const User = require('../src/models/user');
// Mock the User model
jest.mock('../src/models/user');
describe('User API', () => {
// Test creating a new user with a mock
it('should create a user', async () => {
User.mockImplementation(() => ({
save: jest.fn().mockResolvedValueOnce({ name: 'John Doe', email: 'john@example.com' }),
}));
const response = await request(app).post('/users').send({
name: 'John Doe',
email: 'john@example.com',
});
expect(response.status).toBe(201);
expect(response.body.name).toBe('John Doe');
expect(response.body.email).toBe('john@example.com');
});
// Test retrieving a user by email with a mock
it('should get a user by email', async () => {
User.findOne.mockResolvedValueOnce({ name: 'Jane Doe', email: 'jane@example.com' });
const response = await request(app).get(`/users/jane@example.com`);
expect(response.status).toBe(200);
expect(response.body.name).toBe('Jane Doe');
expect(response.body.email).toBe('jane@example.com');
});
// Test returning 404 if user not found with a mock
it('should return 404 if user not found', async () => {
User.findOne.mockResolvedValueOnce(null);
const response = await request(app).get('/users/nonexistent@example.com');
expect(response.status).toBe(404);
});
});
In this updated test file, we use Jest to mock the User model, allowing us to test the routes without interacting with a real database.
Testing Database Interactions
Finally, let's test the actual database interactions without mocking to ensure our MongoDB integration works correctly. We'll use a real MongoDB instance for these tests. The setup and teardown will handle connecting to the database and cleaning up after each test.
Update tests/user.test.js
to test actual database interactions:
const mongoose = require('mongoose');
const request = require('supertest');
const app = require('../src/app');
const User = require('../src/models/user');
// Connect to the MongoDB database before all tests
beforeAll(async () => {
const url = `mongodb://127.0.0.1/integration_testing`;
await mongoose.connect(url, { useNewUrlParser: true, useUnifiedTopology: true });
});
// Clean up the database before each test
beforeEach(async () => {
await User.deleteMany();
});
// Clean up the database and close the connection after all tests
afterAll(async () => {
await mongoose.connection.db.dropDatabase();
await mongoose.connection.close();
});
describe('User API', () => {
// Test creating a new user
it('should create a user', async () => {
const response = await request(app).post('/users').send({
name: 'John Doe',
email: 'john@example.com',
});
expect(response.status).toBe(201);
expect(response.body.name).toBe('John Doe');
expect(response.body.email).toBe('john@example.com');
});
// Test retrieving a user by email
it('should get a user by email', async () => {
const user = new User({ name: 'Jane Doe', email: 'jane@example.com' });
await user.save();
const response = await request(app).get(`/users/jane@example.com`);
expect(response.status).toBe(200);
expect(response.body.name).toBe('Jane Doe');
expect(response.body.email).toBe('jane@example.com');
});
// Test returning 404 if user not found
it('should return 404 if user not found', async () => {
const response = await request(app).get('/users/nonexistent@example.com');
expect(response.status).toBe(404);
});
});
In these tests, we connect to a MongoDB instance before running the tests and clean up the database after each test to ensure a fresh state. This allows us to test real interactions with the database.
Conclusion
Integration testing in Node.js is essential for verifying that different parts of your application work together correctly. In this guide, we covered how to:
- Testing Interactions Between Modules and Components: We created tests to verify that our Express routes, services, and database interact as expected.
- Mocking Dependencies and External Services: We used Jest to mock the User model, allowing us to isolate and test our routes without relying on a real database.
- Testing Database Interactions: We connected to a real MongoDB instance to ensure our application correctly interacts with the database, cleaning up the data after each test to maintain a fresh state.
By following these practices, you can ensure that your Node.js application is robust and that its components work seamlessly together.
Top comments (1)
Thanks for putting up the effort for a nice article.
Just a note, I think as a community we should start putting more thought into what dependencies we chose to include in the examples. Express is a stale framework that has very little progress over the years, Mongoose is something I would advice everyone from using (the problems it has really don't worth what models that only protect the structure on app level). These solutions continue to be popular as we still consider it as a no-brainer to start with, and newcomers see these tutorials and assume there's nothing wron