HTML & JS Topic
Node.js Testing
1. Unit Testing
What is Unit Testing?
Unit testing involves testing individual components or functions in isolation from the rest of the application. Each unit test should be independent and test one specific piece of functionality.
Popular Testing Frameworks
Jest (Most Popular)
javascript
// math.js
function add(a, b) {
return a + b;
}
function subtract(a, b) {
return a - b;
}
// math.test.js
describe('Math operations', () => {
test('adds two numbers correctly', () => {
expect(add(2, 3)).toBe(5);
expect(add(-1, 1)).toBe(0);
});
test('subtracts two numbers correctly', () => {
expect(subtract(5, 3)).toBe(2);
expect(subtract(0, 5)).toBe(-5);
});
});
Mocha + Chai + Sinon
javascript
// test/math.test.js
const { expect } = require('chai');
const { add, subtract } = require('../math');
describe('Math operations', () => {
describe('add()', () => {
it('should add two positive numbers', () => {
expect(add(2, 3)).to.equal(5);
});
it('should handle negative numbers', () => {
expect(add(-1, -1)).to.equal(-2);
});
});
describe('subtract()', () => {
it('should subtract two numbers', () => {
expect(subtract(5, 3)).to.equal(2);
});
});
});
Configuration Examples
Jest Configuration (jest.config.js)
javascript
module.exports = {
testEnvironment: 'node',
collectCoverage: true,
coverageDirectory: 'coverage',
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
}
},
testMatch: ['**/__tests__/**/*.js', '**/?(*.)+(spec|test).js'],
verbose: true,
forceExit: true,
clearMocks: true
};
Mocha Configuration (.mocharc.js)
javascript
module.exports = {
require: ['@babel/register'], // If using ES6
recursive: true,
reporter: 'spec',
timeout: 5000,
exit: true
};
2. Integration Testing
What is Integration Testing?
Integration testing verifies that different modules or services work together correctly. It tests the interaction between components.
Database Integration Testing
javascript
// userService.integration.test.js
const mongoose = require('mongoose');
const User = require('../models/User');
const UserService = require('../services/UserService');
describe('User Service Integration', () => {
beforeAll(async () => {
// Connect to test database
await mongoose.connect('mongodb://localhost:27017/testdb', {
useNewUrlParser: true,
useUnifiedTopology: true
});
});
beforeEach(async () => {
// Clear database before each test
await User.deleteMany({});
});
afterAll(async () => {
await mongoose.connection.close();
});
test('should create and retrieve a user', async () => {
// Arrange
const userData = {
name: 'John Doe',
email: 'john@example.com'
};
// Act
const createdUser = await UserService.createUser(userData);
const retrievedUser = await UserService.getUserById(createdUser._id);
// Assert
expect(retrievedUser.name).toBe(userData.name);
expect(retrievedUser.email).toBe(userData.email);
});
});
Service Integration Testing
javascript
// paymentService.integration.test.js
const PaymentService = require('../services/PaymentService');
const EmailService = require('../services/EmailService');
const UserRepository = require('../repositories/UserRepository');
jest.mock('../services/EmailService');
jest.mock('../repositories/UserRepository');
describe('Payment Service Integration', () => {
beforeEach(() => {
jest.clearAllMocks();
});
test('should process payment and send email', async () => {
// Arrange
const userId = '123';
const amount = 100;
const user = { email: 'user@example.com', name: 'John' };
UserRepository.findById.mockResolvedValue(user);
EmailService.sendPaymentConfirmation.mockResolvedValue(true);
// Act
const result = await PaymentService.processPayment(userId, amount);
// Assert
expect(UserRepository.findById).toHaveBeenCalledWith(userId);
expect(EmailService.sendPaymentConfirmation)
.toHaveBeenCalledWith(user.email, amount);
expect(result.success).toBe(true);
});
});
3. API Testing
Using Supertest (with Express)
javascript
// app.js
const express = require('express');
const app = express();
app.use(express.json());
let users = [];
app.get('/api/users', (req, res) => {
res.json(users);
});
app.post('/api/users', (req, res) => {
const user = { id: users.length + 1, ...req.body };
users.push(user);
res.status(201).json(user);
});
module.exports = app;
// app.test.js
const request = require('supertest');
const app = require('./app');
describe('User API', () => {
beforeEach(() => {
// Reset users array for each test
global.users = [];
});
describe('GET /api/users', () => {
it('should return empty array initially', async () => {
const response = await request(app)
.get('/api/users')
.expect('Content-Type', /json/)
.expect(200);
expect(response.body).toEqual([]);
});
});
describe('POST /api/users', () => {
it('should create a new user', async () => {
const userData = {
name: 'John Doe',
email: 'john@example.com'
};
const response = await request(app)
.post('/api/users')
.send(userData)
.expect('Content-Type', /json/)
.expect(201);
expect(response.body).toMatchObject(userData);
expect(response.body.id).toBeDefined();
});
it('should return 400 for invalid data', async () => {
await request(app)
.post('/api/users')
.send({}) // Empty object
.expect(400);
});
});
});
Advanced API Testing with Authentication
javascript
// auth.test.js
const request = require('supertest');
const app = require('./app');
describe('Authentication API', () => {
let authToken;
beforeAll(async () => {
// Register a test user
await request(app)
.post('/api/auth/register')
.send({
email: 'test@example.com',
password: 'password123'
});
});
test('should login and get token', async () => {
const response = await request(app)
.post('/api/auth/login')
.send({
email: 'test@example.com',
password: 'password123'
})
.expect(200);
expect(response.body.token).toBeDefined();
authToken = response.body.token;
});
test('should access protected route with token', async () => {
const response = await request(app)
.get('/api/protected')
.set('Authorization', `Bearer ${authToken}`)
.expect(200);
expect(response.body.message).toBe('Protected data');
});
test('should reject request without token', async () => {
await request(app)
.get('/api/protected')
.expect(401);
});
});
4. Test Coverage
Understanding Test Coverage Metrics
-
Line Coverage: Percentage of code lines executed
-
Statement Coverage: Percentage of statements executed
-
Branch Coverage: Percentage of decision branches executed
-
Function Coverage: Percentage of functions called
Setting Up Coverage with Jest
json
// package.json
{
"scripts": {
"test": "jest",
"test:coverage": "jest --coverage",
"test:watch": "jest --watch"
},
"jest": {
"collectCoverageFrom": [
"src/**/*.js",
"!src/**/*.test.js",
"!src/**/index.js"
],
"coverageThreshold": {
"global": {
"branches": 80,
"functions": 80,
"lines": 80,
"statements": 80
}
}
}
}
Coverage Configuration
javascript
// jest.config.js with detailed coverage
module.exports = {
collectCoverage: true,
coverageDirectory: 'coverage',
coverageReporters: ['text', 'lcov', 'html'],
collectCoverageFrom: [
'src/**/*.js',
'!src/**/*.spec.js',
'!src/**/*.test.js',
'!src/tests/**',
'!src/**/index.js'
],
coveragePathIgnorePatterns: [
'/node_modules/',
'/coverage/',
'/dist/'
],
coverageThreshold: {
global: {
branches: 70,
functions: 80,
lines: 80,
statements: 80
},
'./src/services/': {
branches: 90,
functions: 90,
lines: 90,
statements: 90
}
}
};
Generating Coverage Reports
bash
# Run tests with coverage
npm test -- --coverage
# Generate HTML report
npm test -- --coverage --coverageReporters=html
# Watch mode with coverage
npm test -- --watch --coverage
5. Mocking & Stubbing
Types of Test Doubles
-
Mocks: Objects that record method calls
-
Stubs: Objects that return predefined responses
-
Spies: Wrappers that record function calls
-
Fakes: Simplified working implementations
Jest Mocking Examples
Function Mocking
javascript
// math.js
const { add } = require('./calculator');
function calculateTotal(a, b) {
return add(a, b) * 1.1; // Add 10% tax
}
// math.test.js
const calculator = require('./calculator');
const { calculateTotal } = require('./math');
jest.mock('./calculator');
test('calculateTotal should use add function', () => {
// Mock implementation
calculator.add.mockReturnValue(100);
const result = calculateTotal(50, 50);
// Verify mock was called
expect(calculator.add).toHaveBeenCalledWith(50, 50);
expect(result).toBe(110); // 100 + 10%
});
Module Mocking
javascript
// emailService.test.js
const axios = require('axios');
const EmailService = require('./emailService');
// Mock axios module
jest.mock('axios');
describe('EmailService', () => {
beforeEach(() => {
jest.clearAllMocks();
});
test('should send email successfully', async () => {
// Setup mock response
axios.post.mockResolvedValue({
data: { success: true, messageId: '123' }
});
const result = await EmailService.sendWelcomeEmail('user@example.com');
expect(axios.post).toHaveBeenCalledWith(
'https://api.emailservice.com/send',
expect.objectContaining({
to: 'user@example.com',
subject: 'Welcome!'
})
);
expect(result.success).toBe(true);
});
test('should handle email failure', async () => {
// Mock error response
axios.post.mockRejectedValue(new Error('Network Error'));
await expect(EmailService.sendWelcomeEmail('user@example.com'))
.rejects.toThrow('Failed to send email');
});
});
Partial Mocking
javascript
// userService.test.js
const UserService = require('./userService');
const db = require('./database');
jest.mock('./database', () => ({
...jest.requireActual('./database'), // Use real implementation
query: jest.fn() // Mock only the query method
}));
describe('UserService with partial mock', () => {
test('should get user with mocked query', async () => {
const mockUser = { id: 1, name: 'John' };
db.query.mockResolvedValue([mockUser]);
const user = await UserService.getUser(1);
expect(db.query).toHaveBeenCalledWith(
'SELECT * FROM users WHERE id = ?',
[1]
);
expect(user).toEqual(mockUser);
});
});
Advanced Mocking Patterns
Mocking Date/Time
javascript
// dateUtils.test.js
const { getCurrentYear, isFutureDate } = require('./dateUtils');
describe('Date utilities with mocked date', () => {
beforeAll(() => {
// Mock Date constructor
jest.useFakeTimers();
jest.setSystemTime(new Date('2024-01-01'));
});
afterAll(() => {
jest.useRealTimers();
});
test('should return current year', () => {
expect(getCurrentYear()).toBe(2024);
});
test('should detect future dates', () => {
expect(isFutureDate('2024-12-31')).toBe(true);
expect(isFutureDate('2023-12-31')).toBe(false);
});
});
Mocking HTTP Requests
javascript
// apiClient.test.js
const axios = require('axios');
const ApiClient = require('./apiClient');
jest.mock('axios');
describe('ApiClient with mocked HTTP', () => {
beforeEach(() => {
axios.create.mockReturnValue({
get: jest.fn(),
post: jest.fn(),
put: jest.fn(),
delete: jest.fn()
});
});
test('should fetch users', async () => {
const mockUsers = [{ id: 1, name: 'John' }];
const mockGet = jest.fn().mockResolvedValue({ data: mockUsers });
axios.create.mockReturnValue({ get: mockGet });
const client = new ApiClient('https://api.example.com');
const users = await client.getUsers();
expect(mockGet).toHaveBeenCalledWith('/users');
expect(users).toEqual(mockUsers);
});
});
Manual Mocks
javascript
// __mocks__/fs.js
const fs = jest.createMockFromModule('fs');
let mockFiles = {};
fs.readFileSync = (path) => {
if (mockFiles[path]) {
return mockFiles[path];
}
throw new Error('File not found');
};
fs.writeFileSync = (path, data) => {
mockFiles[path] = data;
};
fs.__setMockFiles = (files) => {
mockFiles = files;
};
fs.__clearMockFiles = () => {
mockFiles = {};
};
module.exports = fs;
// fileService.test.js
jest.mock('fs');
const fs = require('fs');
describe('FileService with manual mock', () => {
beforeEach(() => {
fs.__clearMockFiles();
});
test('should read file content', () => {
fs.__setMockFiles({
'/test.txt': 'Hello World'
});
const content = fs.readFileSync('/test.txt');
expect(content).toBe('Hello World');
});
});
Best Practices for Mocking
javascript
// Example of good mocking practices
describe('OrderService', () => {
let mockPaymentGateway;
let mockInventoryService;
let mockEmailService;
beforeEach(() => {
// Create fresh mocks for each test
mockPaymentGateway = {
processPayment: jest.fn(),
refundPayment: jest.fn()
};
mockInventoryService = {
checkStock: jest.fn(),
updateStock: jest.fn()
};
mockEmailService = {
sendOrderConfirmation: jest.fn(),
sendShippingNotification: jest.fn()
};
// Reset all mocks
jest.clearAllMocks();
});
test('should process order successfully', async () => {
// Arrange: Setup mock responses
mockPaymentGateway.processPayment.mockResolvedValue({ success: true });
mockInventoryService.checkStock.mockResolvedValue(true);
mockEmailService.sendOrderConfirmation.mockResolvedValue(true);
// Act: Call the function
const orderService = new OrderService(
mockPaymentGateway,
mockInventoryService,
mockEmailService
);
const result = await orderService.processOrder(orderData);
// Assert: Verify interactions
expect(mockPaymentGateway.processPayment).toHaveBeenCalledWith(
orderData.payment
);
expect(mockInventoryService.checkStock).toHaveBeenCalledWith(
orderData.items
);
expect(mockEmailService.sendOrderConfirmation).toHaveBeenCalledWith(
orderData.customer.email
);
expect(result.success).toBe(true);
});
});
6. Testing Strategies & Best Practices
Test Organization Structure
text
src/
├── __tests__/
│ ├── unit/
│ │ ├── services/
│ │ ├── controllers/
│ │ └── utils/
│ ├── integration/
│ │ ├── api/
│ │ └── database/
│ └── e2e/
├── services/
├── controllers/
└── utils/
Test Setup Patterns
javascript
// Common test setup pattern
describe('UserService', () => {
// Test fixtures
const testUser = {
id: 1,
name: 'Test User',
email: 'test@example.com'
};
// Shared mocks
let mockRepository;
let mockValidator;
// Setup before all tests
beforeAll(() => {
// One-time setup (DB connection, etc.)
});
// Setup before each test
beforeEach(() => {
// Reset mocks
mockRepository = {
findById: jest.fn(),
create: jest.fn(),
update: jest.fn()
};
mockValidator = {
validateUser: jest.fn().mockReturnValue({ valid: true })
};
});
// Cleanup after each test
afterEach(() => {
jest.clearAllMocks();
});
// Cleanup after all tests
afterAll(() => {
// Close connections, etc.
});
});
Common Testing Patterns
-
AAA Pattern (Arrange, Act, Assert)
-
Given-When-Then pattern for BDD
-
Test Isolation (each test independent)
-
Meaningful Test Names (describe what should happen)
Performance Tips
javascript
// Avoid common performance pitfalls
// BAD: Repeated setup in each test
test('test 1', () => {
const service = new Service(); // Repeated instantiation
// ...
});
test('test 2', () => {
const service = new Service(); // Repeated instantiation
// ...
});
// GOOD: Shared setup when appropriate
let service;
beforeEach(() => {
service = new Service(); // Created once per test
});
test('test 1', () => {
// Use shared service
});
test('test 2', () => {
// Use shared service
});
7. Continuous Integration Testing
GitHub Actions Example
yaml
# .github/workflows/test.yml
name: Node.js Tests
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [14.x, 16.x, 18.x]
services:
mongodb:
image: mongo:latest
ports:
- 27017:27017
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 linting
run: npm run lint
- name: Run unit tests
run: npm test
- name: Run integration tests
run: npm run test:integration
env:
MONGODB_URI: mongodb://localhost:27017/testdb
- name: Upload coverage
uses: codecov/codecov-action@v2
with:
file: ./coverage/lcov.info
This comprehensive guide covers all aspects of Node.js testing.
Comments (0)
No comments yet. Be the first to comment!