Introduction

Testing ensures your code works as expected and deployment gets it to users. In this lesson, you'll learn unit testing with Jest, integration testing, API testing with Supertest, CI/CD pipelines, and deployment strategies for production environments.

Learning Objectives

  • Write unit tests with Jest
  • Implement integration tests for APIs
  • Test React components effectively
  • Set up continuous integration (CI)
  • Deploy applications to cloud platforms
  • Monitor and debug production applications

Prerequisites

  • Completion of Lessons 01-10
  • Complete full-stack application built
  • Understanding of Git and version control

Estimated Time

3.5 hours (including practice exercises)

Unit Testing with Jest

Terminal
npm install --save-dev jest @types/jest
package.json
{
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage"
  },
  "jest": {
    "testEnvironment": "node",
    "coveragePathIgnorePatterns": ["/node_modules/"]
  }
}

Writing Unit Tests

utils/math.js
exports.add = (a, b) => a + b;
exports.subtract = (a, b) => a - b;
exports.multiply = (a, b) => a * b;
exports.divide = (a, b) => {
  if (b === 0) throw new Error('Division by zero');
  return a / b;
};
utils/math.test.js
const { add, subtract, multiply, divide } = require('./math');

describe('Math utilities', () => {
  test('adds two numbers', () => {
    expect(add(2, 3)).toBe(5);
    expect(add(-1, 1)).toBe(0);
  });
  
  test('subtracts two numbers', () => {
    expect(subtract(5, 3)).toBe(2);
  });
  
  test('multiplies two numbers', () => {
    expect(multiply(3, 4)).toBe(12);
  });
  
  test('divides two numbers', () => {
    expect(divide(10, 2)).toBe(5);
  });
  
  test('throws error on division by zero', () => {
    expect(() => divide(10, 0)).toThrow('Division by zero');
  });
});

API Testing with Supertest

Terminal
npm install --save-dev supertest
tests/api/users.test.js
const request = require('supertest');
const app = require('../../server');
const User = require('../../models/User');

describe('User API', () => {
  beforeEach(async () => {
    await User.deleteMany({});
  });
  
  describe('POST /api/users', () => {
    test('creates a new user', async () => {
      const userData = {
        name: 'John Doe',
        email: '[email protected]',
        password: 'password123'
      };
      
      const response = await request(app)
        .post('/api/users')
        .send(userData)
        .expect(201);
      
      expect(response.body.success).toBe(true);
      expect(response.body.data.email).toBe(userData.email);
    });
    
    test('returns 400 for invalid data', async () => {
      const response = await request(app)
        .post('/api/users')
        .send({ name: 'John' })
        .expect(400);
      
      expect(response.body.success).toBe(false);
    });
  });
  
  describe('GET /api/users', () => {
    test('returns all users', async () => {
      await User.create([
        { name: 'User 1', email: '[email protected]', password: 'pass' },
        { name: 'User 2', email: '[email protected]', password: 'pass' }
      ]);
      
      const response = await request(app)
        .get('/api/users')
        .expect(200);
      
      expect(response.body.data).toHaveLength(2);
    });
  });
});

Testing with Database

tests/setup.js
const mongoose = require('mongoose');

beforeAll(async () => {
  await mongoose.connect(process.env.TEST_MONGODB_URI);
});

afterAll(async () => {
  await mongoose.connection.close();
});

afterEach(async () => {
  const collections = mongoose.connection.collections;
  for (const key in collections) {
    await collections[key].deleteMany();
  }
});

CI/CD with GitHub Actions

.github/workflows/test.yml
name: Run Tests

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]

jobs:
  test:
    runs-on: ubuntu-latest
    
    services:
      mongodb:
        image: mongo:latest
        ports:
          - 27017:27017
    
    steps:
      - uses: actions/checkout@v3
      
      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'
      
      - name: Install dependencies
        run: npm ci
      
      - name: Run tests
        run: npm test
        env:
          TEST_MONGODB_URI: mongodb://localhost:27017/test
      
      - name: Upload coverage
        uses: codecov/codecov-action@v3

Environment Configuration

.env.example
NODE_ENV=development
PORT=3000
MONGODB_URI=mongodb://localhost:27017/myapp
JWT_SECRET=your-secret-key
JWT_EXPIRE=7d
config/config.js
module.exports = {
  development: {
    port: process.env.PORT || 3000,
    mongoUri: process.env.MONGODB_URI,
    jwtSecret: process.env.JWT_SECRET
  },
  production: {
    port: process.env.PORT || 5000,
    mongoUri: process.env.MONGODB_URI,
    jwtSecret: process.env.JWT_SECRET
  },
  test: {
    port: 3001,
    mongoUri: process.env.TEST_MONGODB_URI,
    jwtSecret: 'test-secret'
  }
};

Deployment to Heroku

Terminal
# Install Heroku CLI
# Login to Heroku
heroku login

# Create app
heroku create my-app-name

# Add MongoDB
heroku addons:create mongolab

# Set environment variables
heroku config:set JWT_SECRET=your-secret
heroku config:set NODE_ENV=production

# Deploy
git push heroku main

# View logs
heroku logs --tail
Procfile
web: node server.js

Docker Deployment

Dockerfile
FROM node:18-alpine

WORKDIR /app

COPY package*.json ./

RUN npm ci --only=production

COPY . .

EXPOSE 3000

CMD ["node", "server.js"]
docker-compose.yml
version: '3.8'

services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      - MONGODB_URI=mongodb://mongo:27017/myapp
      - JWT_SECRET=secret
    depends_on:
      - mongo
  
  mongo:
    image: mongo:latest
    ports:
      - "27017:27017"
    volumes:
      - mongo-data:/data/db

volumes:
  mongo-data:

Monitoring with PM2

Terminal
# Install PM2
npm install -g pm2

# Start app
pm2 start server.js --name my-app

# Monitor
pm2 monit

# View logs
pm2 logs

# Restart
pm2 restart my-app

# Stop
pm2 stop my-app
ecosystem.config.js
module.exports = {
  apps: [{
    name: 'my-app',
    script: './server.js',
    instances: 'max',
    exec_mode: 'cluster',
    env: {
      NODE_ENV: 'production'
    }
  }]
};

Key Takeaways

  • Write tests before deploying to production
  • Use Jest for unit and integration testing
  • Supertest simplifies API testing
  • CI/CD automates testing and deployment
  • Environment variables manage configuration
  • Docker containers ensure consistency
  • PM2 manages Node.js processes in production
  • Monitor applications for errors and performance

Practice Exercises

Exercise 1: Write Test Suite

Task
// Create tests for:
// - User registration
// - User login
// - Protected routes
// - Error handling
// Aim for 80%+ coverage

Exercise 2: Setup CI/CD

Task
// Configure GitHub Actions to:
// - Run tests on every push
// - Check code coverage
// - Deploy to staging on merge to develop
// - Deploy to production on merge to main

Exercise 3: Docker Setup

Task
// Create Docker setup with:
// - Node.js app container
// - MongoDB container
// - Nginx reverse proxy
// - Docker Compose orchestration

Exercise 4: Deploy to Cloud

Task
// Deploy your app to:
// - Heroku (or)
// - AWS EC2 (or)
// - DigitalOcean
// Configure environment variables
// Set up monitoring

What's Next?

In Lesson 12, you'll build a complete e-commerce platform from scratch. This capstone project integrates everything you've learned: React frontend, Node.js backend, MongoDB database, authentication, payment processing, and deployment.