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
Additional Resources
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.