Menu

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

  1. Mocks: Objects that record method calls

  2. Stubs: Objects that return predefined responses

  3. Spies: Wrappers that record function calls

  4. 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

  1. AAA Pattern (Arrange, Act, Assert)

  2. Given-When-Then pattern for BDD

  3. Test Isolation (each test independent)

  4. 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!

LecturreAI Assistant

Hello! I'm LecturreAI

What would you like to learn today?