DEV Community

Claradev32
Claradev32

Posted on

Integration Testing with Node.js

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
Enter fullscreen mode Exit fullscreen mode

Install the required dependencies:

npm install --save express mongoose
npm install --save-dev jest supertest
Enter fullscreen mode Exit fullscreen mode

Update the package.json to add a test script:

"scripts": {
  "test": "jest"
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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 };
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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);
  });
});
Enter fullscreen mode Exit fullscreen mode

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);
  });
});
Enter fullscreen mode Exit fullscreen mode

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);
  });
});
Enter fullscreen mode Exit fullscreen mode

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 (0)