Skip to main content

JavaScript Integration Testing

Introduction to Integration Testing

Integration testing is a critical phase in the testing pyramid that focuses on verifying that different modules or services in your application work well together. Unlike unit tests that isolate individual components, integration tests examine the interactions between integrated components to expose issues in their interfaces and how they work together.

In the JavaScript ecosystem, integration testing helps us ensure that:

  • Different parts of our application communicate correctly
  • Data flows properly between components
  • External dependencies like databases or APIs work as expected
  • Application workflows function correctly from end to end

Integration testing bridges the gap between unit testing (testing individual functions and modules) and end-to-end testing (testing the entire application stack).

Why Integration Testing Matters

As applications grow in complexity, unit tests alone can't catch all issues. Components might work fine in isolation but fail when integrated. Integration tests help you:

  • Identify bugs in component interactions early
  • Validate application workflows across multiple components
  • Ensure database operations work correctly
  • Test API endpoints with real requests
  • Verify third-party service integrations

Setting Up Integration Tests

Let's explore how to set up integration tests for a JavaScript application. We'll use Jest as our test runner and Supertest for API testing.

First, let's install the necessary dependencies:

bash
npm install --save-dev jest supertest

Update your package.json to include a test script:

json
{
"scripts": {
"test": "jest",
"test:integration": "jest --config jest.integration.config.js"
}
}

Create a separate Jest configuration for integration tests:

javascript
// jest.integration.config.js
module.exports = {
testMatch: ['**/*.integration.test.js'],
testTimeout: 10000, // Integration tests may take longer
setupFilesAfterEnv: ['./test/setup-integration.js']
};

Testing REST APIs

One common integration testing scenario is testing REST APIs. Let's create an integration test for a simple Express API:

javascript
// app.js - Our Express application
const express = require('express');
const app = express();

app.use(express.json());

app.get('/api/users', (req, res) => {
res.json([
{ id: 1, name: 'John Doe' },
{ id: 2, name: 'Jane Smith' }
]);
});

app.post('/api/users', (req, res) => {
const { name } = req.body;
if (!name) {
return res.status(400).json({ error: 'Name is required' });
}
return res.status(201).json({ id: 3, name });
});

module.exports = app;

Now let's create an integration test for our API endpoints:

javascript
// api.integration.test.js
const request = require('supertest');
const app = require('./app');

describe('User API', () => {
describe('GET /api/users', () => {
it('should return all users', async () => {
const response = await request(app).get('/api/users');

expect(response.status).toBe(200);
expect(response.body).toHaveLength(2);
expect(response.body[0]).toHaveProperty('id');
expect(response.body[0]).toHaveProperty('name');
});
});

describe('POST /api/users', () => {
it('should create a new user', async () => {
const userData = { name: 'Test User' };

const response = await request(app)
.post('/api/users')
.send(userData);

expect(response.status).toBe(201);
expect(response.body.name).toBe('Test User');
expect(response.body).toHaveProperty('id');
});

it('should return 400 if name is missing', async () => {
const response = await request(app)
.post('/api/users')
.send({});

expect(response.status).toBe(400);
expect(response.body).toHaveProperty('error');
});
});
});

Testing Database Interactions

For integration tests involving databases, you'll need to:

  1. Set up a test database (often in memory or a dedicated test instance)
  2. Seed test data before tests run
  3. Clean up after tests complete

Here's an example using MongoDB with Mongoose:

javascript
// user.model.js
const mongoose = require('mongoose');

const userSchema = new mongoose.Schema({
name: { type: String, required: true },
email: { type: String, required: true, unique: true }
});

module.exports = mongoose.model('User', userSchema);
javascript
// user.service.js
const User = require('./user.model');

async function createUser(userData) {
const user = new User(userData);
await user.save();
return user;
}

async function getAllUsers() {
return await User.find();
}

module.exports = {
createUser,
getAllUsers
};

Now let's create our integration test:

javascript
// database.integration.test.js
const mongoose = require('mongoose');
const { MongoMemoryServer } = require('mongodb-memory-server');
const userService = require('./user.service');

let mongoServer;

beforeAll(async () => {
// Set up in-memory MongoDB server
mongoServer = await MongoMemoryServer.create();
const uri = mongoServer.getUri();
await mongoose.connect(uri);
});

afterAll(async () => {
await mongoose.disconnect();
await mongoServer.stop();
});

beforeEach(async () => {
// Clear database between tests
await mongoose.connection.dropDatabase();
});

describe('User Service Integration Tests', () => {
it('should create a new user', async () => {
// Arrange
const userData = { name: 'Test User', email: 'test@example.com' };

// Act
const newUser = await userService.createUser(userData);

// Assert
expect(newUser._id).toBeDefined();
expect(newUser.name).toBe('Test User');
expect(newUser.email).toBe('test@example.com');
});

it('should retrieve all users', async () => {
// Arrange
await userService.createUser({ name: 'User 1', email: 'user1@example.com' });
await userService.createUser({ name: 'User 2', email: 'user2@example.com' });

// Act
const users = await userService.getAllUsers();

// Assert
expect(users).toHaveLength(2);
expect(users[0].name).toBe('User 1');
expect(users[1].name).toBe('User 2');
});
});

Testing Component Integration

For frontend applications built with frameworks like React, Vue, or Angular, integration tests verify that multiple components work well together. Here's an example using React Testing Library:

jsx
// UserList.js
import React from 'react';

const UserList = ({ users, onSelect }) => (
<div>
<h2>Users</h2>
<ul>
{users.map(user => (
<li key={user.id} onClick={() => onSelect(user)}>
{user.name}
</li>
))}
</ul>
</div>
);

// UserDetail.js
import React from 'react';

const UserDetail = ({ user }) => (
<div>
<h2>User Details</h2>
{user ? (
<div>
<p>Name: {user.name}</p>
<p>Email: {user.email}</p>
</div>
) : (
<p>Select a user to view details</p>
)}
</div>
);

// UserManagement.js - Parent component that integrates the two components
import React, { useState } from 'react';
import UserList from './UserList';
import UserDetail from './UserDetail';

const UserManagement = ({ users }) => {
const [selectedUser, setSelectedUser] = useState(null);

return (
<div className="user-management">
<UserList users={users} onSelect={setSelectedUser} />
<UserDetail user={selectedUser} />
</div>
);
};

export default UserManagement;

Now let's write an integration test:

javascript
// userManagement.integration.test.js
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import UserManagement from './UserManagement';

const mockUsers = [
{ id: 1, name: 'John Doe', email: 'john@example.com' },
{ id: 2, name: 'Jane Smith', email: 'jane@example.com' }
];

describe('User Management Integration', () => {
test('displays user details when a user is selected from the list', () => {
// Arrange
render(<UserManagement users={mockUsers} />);

// Initial state - should show placeholder text in details
expect(screen.getByText('Select a user to view details')).toBeInTheDocument();

// Act - click on the first user
fireEvent.click(screen.getByText('John Doe'));

// Assert - should now show details for John Doe
expect(screen.getByText('Name: John Doe')).toBeInTheDocument();
expect(screen.getByText('Email: john@example.com')).toBeInTheDocument();

// Act again - click on the second user
fireEvent.click(screen.getByText('Jane Smith'));

// Assert again - should now show details for Jane Smith
expect(screen.getByText('Name: Jane Smith')).toBeInTheDocument();
expect(screen.getByText('Email: jane@example.com')).toBeInTheDocument();
});
});

Testing Workflows and User Journeys

Integration tests for workflows verify that multi-step processes work correctly from start to finish. Here's an example for a user authentication workflow:

javascript
// auth.integration.test.js
const request = require('supertest');
const app = require('./app');

describe('Authentication Flow', () => {
it('should register, login and access protected resources', async () => {
// Step 1: Register a new user
const registerResponse = await request(app)
.post('/api/auth/register')
.send({
email: 'integration@test.com',
password: 'secure123',
name: 'Integration Test'
});

expect(registerResponse.status).toBe(201);
expect(registerResponse.body).toHaveProperty('message', 'User registered successfully');

// Step 2: Log in with the new user
const loginResponse = await request(app)
.post('/api/auth/login')
.send({
email: 'integration@test.com',
password: 'secure123'
});

expect(loginResponse.status).toBe(200);
expect(loginResponse.body).toHaveProperty('token');
const token = loginResponse.body.token;

// Step 3: Access protected resource with JWT token
const profileResponse = await request(app)
.get('/api/profile')
.set('Authorization', `Bearer ${token}`);

expect(profileResponse.status).toBe(200);
expect(profileResponse.body).toHaveProperty('name', 'Integration Test');

// Step 4: Attempt to access protected resource with invalid token
const unauthorizedResponse = await request(app)
.get('/api/profile')
.set('Authorization', 'Bearer invalidtoken');

expect(unauthorizedResponse.status).toBe(401);
});
});

Best Practices for Integration Testing

  1. Isolate the test environment: Use separate databases, mock external APIs, and reset state between tests.

  2. Test realistic scenarios: Create tests that mimic real user interactions and workflows.

  3. Mind the scope: Integration tests should focus on how components integrate, not every detail of individual components.

  4. Balance thoroughness and speed: Integration tests are slower than unit tests, so be strategic about what to test.

  5. Use appropriate tools: Choose testing frameworks designed for integration testing that support your stack.

  6. Plan for asynchronous operations: Many integrations involve promises, callbacks, or timeouts that need careful handling.

  7. Make tests deterministic: Avoid random data or timing-dependent tests that might cause flaky results.

  8. Clean up after tests: Ensure each test leaves the environment as it found it to prevent interference between tests.

Common Integration Testing Tools

Here's a list of popular tools for JavaScript integration testing:

  • Jest: Full-featured test runner with built-in assertions and mocking
  • Supertest: HTTP assertions for testing APIs
  • MongoDB Memory Server: In-memory MongoDB server for database testing
  • MSW (Mock Service Worker): API mocking for browser and Node.js
  • Cypress: End-to-end testing framework that can also be used for integration testing
  • Playwright: Browser automation library for cross-browser testing
  • Testing Library: Family of libraries for testing UI components in a user-centric way

Example Project Structure

A well-organized integration testing setup might look like this:

project/
├── src/
│ ├── models/
│ ├── services/
│ ├── controllers/
│ └── app.js
├── test/
│ ├── unit/
│ ├── integration/
│ │ ├── api.integration.test.js
│ │ ├── database.integration.test.js
│ │ └── workflow.integration.test.js
│ └── setup-integration.js
├── jest.config.js
└── jest.integration.config.js

Summary

Integration testing is a vital part of JavaScript testing that ensures different parts of your application work well together. Unlike unit tests that focus on isolated components, integration tests verify the interactions between components, services, and external dependencies.

We've explored various aspects of integration testing in JavaScript:

  • Setting up an integration test environment
  • Testing REST APIs with Supertest
  • Testing database interactions
  • Testing component integration in frontend applications
  • Verifying multi-step workflows
  • Best practices for effective integration testing

By incorporating integration tests into your testing strategy, you can catch issues that might slip through unit tests, leading to a more robust and reliable application.

Additional Resources

Exercises

  1. Create integration tests for a TODO API that supports CRUD operations
  2. Write integration tests for a login form that validates inputs and submits to an API
  3. Develop integration tests for a shopping cart workflow (add items, update quantities, checkout)
  4. Set up a test database with test data and write integration tests for a database service
  5. Create tests for a multi-step form that validates each step before proceeding

By practicing these exercises, you'll gain valuable experience in integration testing and improve the quality of your JavaScript applications.



If you spot any mistakes on this website, please let me know at feedback@compilenrun.com. I’d greatly appreciate your feedback! :)