🧪Advanced Testing & CI/CD

Learn to write scalable tests and integrate them into CI/CD pipelines.

🧬 Integration & E2E Tests

Integration tests verify how different components of your application work together, while end-to-end (E2E) tests ensure the entire system behaves as expected from the user's perspective. These tests are crucial for catching issues that unit tests might miss.

💡 Common Testing Tools

Here are some popular tools for integration and E2E testing in Node.js:

  • Supertest: Perfect for testing HTTP APIs.
  • Playwright: A modern tool for browser automation and E2E tests.
  • Cypress: Great for frontend E2E testing with real-time reloading.

💡 Testing Strategy

A solid testing strategy includes:

  • Testing all critical API endpoints.
  • Simulating real user interactions for E2E tests.
  • Handling edge cases and error conditions.

Example: Testing an HTTP API with Supertest

const request = require('supertest');
const app = require('../app');

describe('GET /users', () => {
  it('should return a list of users with status code 200', async () => {
    const response = await request(app).get('/users');
    expect(response.statusCode).toBe(200);
    expect(response.body).toHaveLength(5);
  });
});

Example: E2E Test with Playwright

const { chromium } = require('playwright');

async function testLoginPage() {
  const browser = await chromium.launch();
  const context = await browser.newContext();
  const page = await context.newPage();
  
  await page.goto('http://localhost:3000/login');
  await page.fill('[name="username"]', 'test@example.com');
  await page.fill('[name="password"]', 'password123');
  await page.click('button[type="submit"]');
  
  // Expect successful login
  const dashboardTitle = await page.title();
  expect(dashboardTitle).toBe('Dashboard');
}

testLoginPage();

💡 Best Practices for Integration and E2E Tests

  • Keep tests independent and avoid shared state.
  • Use mocking for external dependencies.
  • Parallelize test execution to save time.
  • Write maintainable and readable test code.

💡 CI/CD Integration

Integration and E2E tests should be run as part of your CI/CD pipeline to catch issues early. Tools like Jenkins, GitHub Actions, or CircleCI can automate this process.

name: Node.js Tests
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-node@v3
        with:
          node-version: '16.x'
      - run: npm install
      - run: npm test
        env:
          CI: true

🛠 Mocking & Stubbing

Welcome to the chapter on Mocking & Stubbing! In this section, we'll explore how to use ⌘jest.mock⌅, test doubles, and API mocking services to create robust and reliable tests for your Node.js applications.

💡 What is Mocking?

Mocking involves creating fake implementations of dependencies to isolate the code being tested. This helps ensure that your tests are reliable, fast, and not dependent on external systems.

💡 Key Concepts

  • Mocks: Implementations that mimic the behavior of real components for testing purposes.
  • Stubs: Simplified implementations that return predefined values.
  • Spies: Objects that monitor function calls and arguments without modifying behavior.
  • Test Doubles: Generic term for any replacement component used during testing.

💡 Using jest.mock()

Jest provides a powerful mocking utility with ⌘jest.mock⌅. This function allows you to mock modules, functions, and APIs in your tests.

jest.mock('module-path');

const mockedFunction = jest.fn();
mockedFunction.mockImplementation(() => 'test value');

💡 Advanced Mocking Techniques

Partial mocking allows you to mock specific methods of an object while keeping others real. This is useful for testing complex interactions.

const module = require('module-path');
jest.mock('module-path');

module.methodToMock = jest.fn(() => 'mocked value');

💡 Best Practices for Mocking

  • Always mock external dependencies like databases, APIs, or third-party services.
  • Keep mocks simple and focused on the specific behavior being tested.
  • Use descriptive names for your mocks to make tests more readable.
  • Clean up mocks after each test using ⌘afterEach⌅.

💡 Real-World Applications of Mocking

Mocks are essential for testing in real-world applications. Common use cases include: API calls, third-party libraries, complex business logic, and UI components.

Common Mistakes to Avoid

  • Avoid over-mocking. Only mock what is necessary for the test.
  • Don't forget to clear mocks with afterEach⌅ or beforeEach⌅.
  • Never mix test doubles across different test cases.
  • Always verify that mocks are properly implemented and behave as expected.

🤖 CI/CD with GitHub Actions & Docker

Welcome to the world of Continuous Integration (CI) and Continuous Deployment (CD)! In this chapter, we'll explore how to automate your testing and deployment pipelines using GitHub Actions and Docker. By the end of this chapter, you'll be able to create robust, scalable, and efficient CI/CD pipelines for your Node.js applications.

💡 What is CI/CD?

CI/CD stands for Continuous Integration and Continuous Deployment. It's a practice where code changes are automatically verified, tested, and deployed. This ensures that your application is always in a deployable state.

  • Continuous Integration: Automate testing of code changes as they are committed
  • Continuous Deployment: Automatically deploy passing builds to production

💡 Why Use GitHub Actions?

GitHub Actions is a powerful platform for automating workflows directly in your GitHub repositories. It integrates seamlessly with other services and provides a simple, declarative way to define your CI/CD pipelines.

  • Native integration with GitHub features like pull requests and issues
  • Supports custom workflows using YAML configuration
  • Can trigger on various events like push, pull request, or schedule
name: CI

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

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v2
      - name: Set up Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '16.x'
      - name: npm install
        run: npm install
      - name: Run tests
        run: npm test

💡 Docker Basics for CI/CD

Docker is essential for creating consistent deployment environments. It allows you to package your application and its dependencies into a single container that can run anywhere.

  • Containers vs. Virtual Machines: Containers are lightweight and share the host OS kernel
  • Docker Images: Immutable, layered templates for containers
  • Dockerfiles: Define how your image should be built
# Use an official Node.js runtime as a parent image
FROM node:16

# Set the working directory
WORKDIR /app

# Copy package files first to leverage Docker cache
COPY package*.json ./

# Install dependencies
RUN npm install

# Copy application source code
COPY . .

# Make port 3000 available outside container
EXPOSE 3000

# Run the app when container launches
CMD ["node", "server.js"]

💡 Implementing CI/CD for Node.js Applications

Let's put it all together! We'll create a GitHub Actions workflow that tests our Node.js application and deploys it using Docker.

  • Set up your repository with the necessary files (Dockerfile, package.json)
  • Create a .github/workflows/ci-cd.yml file
  • Test locally before committing to ensure everything works
name: Node.js CI

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

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v2
      - name: Set up Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '16.x'
          cache: npm
      - name: Install dependencies
        run: npm install
      - name: Run tests
        run: npm test
      - name: Build Docker image
        run: docker build .
      - name: Push Docker image
        env:
          REGISTRY_TOKEN: ${{ secrets.GITHUB_REGISTY_TOKEN }}
        run: |
          echo "${{ steps.build_docker.outputs.image-name }}"

💡 Best Practices for CI/CD Pipelines

  • Keep your workflows small and focused
  • Test everything before deploying
  • Use environment variables for sensitive data
  • Implement rollback strategies
  • Monitor your pipelines with tools like Datadog or AWS CloudWatch

💡 Real-World Applications

CI/CD is essential for modern software development. It enables teams to deliver code faster and more reliably. For example, a Node.js application with microservices can use GitHub Actions to test each service individually before deploying them as Docker containers in production.

Quiz

Question 1 of 14

What is the main difference between integration tests and E2E tests?

  • Integration tests check individual units of code, while E2E tests check the entire system.
  • Integration tests focus on component interactions, while E2E tests simulate user workflows.
  • Integration tests are faster than E2E tests.
  • Integration tests require a testing framework.