Integration Testing with Node.js
Tags: webdev, typescript, integrationtest, opensource
Published: 2024-05-18 00:48:58

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:

```javascript
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.