5 Topics
Chapter•45 Minutes

Automated API Testing

Configure automated testing, write HTTP integration tests using Supertest, test CRUD endpoints, and handle database isolation.

Understand API Testing

API testing verifies that backend endpoints behave as expected.

It helps identify issues before the application reaches production.

Common API testing categories include:

Unit testing

Tests individual functions or modules in isolation.

Integration testing

Tests how multiple application components work together.

End-to-end testing

Tests complete application workflows through the public interface.

For our REST API, we will use Supertest to send HTTP requests directly to the Express application.

We will use Jest as the test runner.

Because the application uses ES Modules, Jest requires compatible configuration.

Key Takeaways
  • API testing verifies endpoint behavior.

  • Integration tests validate multiple connected components.

  • Supertest allows HTTP testing without manually opening a browser.

Configure Jest for ES Modules

Our application uses ES Modules, so configure Jest to run in ESM mode.

Update the package.json scripts.

The experimental VM modules flag is used by Jest for its native ESM support in this configuration.

Use a Jest release compatible with your Node.js version.

Alternative Testing Setup

If you prefer to avoid Jest's ESM configuration complexity, Node.js provides a built-in test runner through node:test.

package.json Test Configuration
{
  "scripts": {
    "start": "node src/server.js",
    "dev": "node --watch src/server.js",
    "test": "NODE_OPTIONS=--experimental-vm-modules jest --runInBand"
  }
}

Configures Jest to execute in ESM mode and runs tests sequentially. On Windows, use cross-env or a platform-specific environment variable command if required.

Key Takeaways
  • Jest requires ESM-compatible configuration.

  • The VM modules flag enables native ESM support in this setup.

  • Keep testing configuration separate from production scripts.

Create an Isolated Test Database

Automated tests should not modify your development or production database.

Create a separate test database.

For example:

MONGODB_TEST_URI=mongodb://127.0.0.1:27017/rest_api_test

A separate database allows tests to create and delete test records without affecting normal application data.

For larger test suites, consider an isolated MongoDB instance or a dedicated test database per test worker.

Database Isolation

Never execute test cleanup commands against a production database. Verify the active database name before dropping collections or deleting test data.

.env.test
NODE_ENV=test
MONGODB_TEST_URI=mongodb://127.0.0.1:27017/rest_api_test
JWT_SECRET=test-only-secret

Defines environment variables for local automated testing. Never reuse production credentials or databases in tests.

Key Takeaways
  • Use a separate test database.

  • Do not run destructive tests against production.

  • Keep test configuration isolated.

Write User API Integration Tests

Create tests/users.test.js.

Supertest accepts an Express application and allows you to send HTTP requests without manually starting the application on a fixed port.

We will test:

  1. Health endpoint.
  2. User registration.
  3. Login.
  4. Invalid login.
  5. Protected endpoint access.

For this example, assume the test setup connects to the isolated test database and clears test records between test runs.

tests/users.test.js
import request from 'supertest';
import mongoose from 'mongoose';
import app from '../src/app.js';
import { User } from '../src/models/user.model.js';
import { connectDatabase } from '../src/config/database.js';

beforeAll(async () => {
  process.env.MONGODB_URI = process.env.MONGODB_TEST_URI;
  await connectDatabase();
});

afterEach(async () => {
  await User.deleteMany({});
});

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

describe('Authentication API', () => {
  test('should register a new user', async () => {
    const response = await request(app)
      .post('/api/v1/auth/register')
      .send({
        name: 'John Doe',
        email: 'john@example.com',
        password: 'SecurePassword123!'
      });

    expect(response.status).toBe(201);
    expect(response.body.success).toBe(true);
    expect(response.body.data.password).toBeUndefined();
  });

  test('should reject invalid login credentials', async () => {
    const response = await request(app)
      .post('/api/v1/auth/login')
      .send({
        email: 'unknown@example.com',
        password: 'wrong-password'
      });

    expect(response.status).toBe(401);
  });
});

Creates integration tests using Supertest. The example assumes the test environment is configured before Jest imports the application and that the authentication routes are registered.

Key Takeaways
  • Supertest can test Express applications directly.

  • Use an isolated database for integration tests.

  • Verify both successful and unsuccessful API requests.

Test Validation and Authorization

A complete test suite should also verify invalid input and access restrictions.

Important test cases include:

  • Registration with missing name.
  • Registration with invalid email.
  • Registration with a short password.
  • Duplicate email registration.
  • Login with incorrect credentials.
  • Accessing protected routes without a token.
  • Accessing administrator routes as a normal user.
  • Updating another user's profile.

Negative test cases are particularly important because they verify that the API rejects unauthorized and invalid operations.

Test Invalid Registration
test('should reject an invalid email address', async () => {
  const response = await request(app)
    .post('/api/v1/auth/register')
    .send({
      name: 'John Doe',
      email: 'invalid-email',
      password: 'SecurePassword123!'
    });

  expect(response.status).toBe(400);
  expect(response.body.success).toBe(false);
});

test('should reject an unauthenticated request', async () => {
  const response = await request(app)
    .get('/api/v1/users');

  expect(response.status).toBe(401);
});

Verifies that invalid registration data and unauthenticated requests are rejected.

Key Takeaways
  • Test invalid inputs as well as successful requests.

  • Verify that protected endpoints reject unauthenticated clients.

  • Test ownership and role-based restrictions.