Testing API Endpoints

Comprehensive strategies and tools for effectively testing your API

The Importance of API Testing

APIs are the connective tissue of modern applications, making their reliability critical. Think of APIs as the nervous system of your application ecosystem – if they fail, the entire organism suffers.

graph LR A[Frontend] <--> B[API] B <--> C[Database] B <--> D[Third-party Services] B <--> E[Authentication] B <--> F[Business Logic] style B fill:#f8d7da,stroke:#721c24 style A fill:#cce5ff,stroke:#004085 style C fill:#d4edda,stroke:#155724 style D fill:#fff3cd,stroke:#856404 style E fill:#d1ecf1,stroke:#0c5460 style F fill:#d1ecf1,stroke:#0c5460

Why API Testing Deserves Special Attention

Real-World API Failure Impact

In 2020, a leading financial services company experienced an API outage that affected millions of users for over 12 hours. The root cause was a simple schema change that wasn't properly tested. This led to inconsistent data validation behaviors between their development and production environments. The incident cost them millions in lost transactions and damaged their reputation with both partners and customers.

Comprehensive API testing would have caught this issue before it reached production.

API Testing Levels

Like a building needs inspection at different stages of construction, APIs require testing at multiple levels.

graph TD A[API Testing Levels] --> B[Unit Testing] A --> C[Integration Testing] A --> D[Functional Testing] A --> E[Contract Testing] A --> F[Load Testing] A --> G[Security Testing] style A fill:#f5f5f5,stroke:#333333 style B fill:#cce5ff,stroke:#004085 style C fill:#d1e7dd,stroke:#0f5132 style D fill:#fff3cd,stroke:#856404 style E fill:#d1ecf1,stroke:#0c5460 style F fill:#f8d7da,stroke:#721c24 style G fill:#e2e3e5,stroke:#41464b

Unit Testing

Tests individual API endpoint handlers, controllers, or resolver functions in isolation.

Integration Testing

Tests how API endpoints interact with each other and with dependencies.

Functional Testing

Tests complete API workflows from the perspective of API consumers.

Contract Testing

Ensures that the API adheres to its defined contract or specification.

Load/Performance Testing

Tests API behavior under various load conditions.

Security Testing

Identifies vulnerabilities in API endpoints and authentication mechanisms.

When Each Level is Important

Consider an e-commerce payment API:

  • Unit Testing: Test that the payment processor correctly calculates tax and shipping
  • Integration Testing: Verify that a successful payment updates inventory and creates an order
  • Functional Testing: Test the complete checkout flow from cart to order confirmation
  • Contract Testing: Ensure the payment response includes all required fields in the documented format
  • Load Testing: Verify the API can handle Black Friday sales volume
  • Security Testing: Check that payment information is encrypted and card details aren't stored improperly

Setting Up Your API Testing Environment

Express API Testing Setup

Let's create a testable structure for an Express API:

// src/app.js - Separate Express app from server
const express = require('express');
const bodyParser = require('body-parser');
const routes = require('./routes');
const errorMiddleware = require('./middleware/error');

const app = express();

app.use(bodyParser.json());
app.use('/api', routes);
app.use(errorMiddleware);

module.exports = app;
// src/server.js - Server startup
const app = require('./app');
const config = require('./config');
const db = require('./db');

async function startServer() {
  try {
    await db.connect();
    
    const server = app.listen(config.port, () => {
      console.log(`Server running on port ${config.port}`);
    });
    
    // Graceful shutdown
    process.on('SIGTERM', () => {
      console.log('SIGTERM received, shutting down gracefully');
      server.close(async () => {
        await db.disconnect();
        console.log('Process terminated');
      });
    });
  } catch (error) {
    console.error('Failed to start server:', error);
    process.exit(1);
  }
}

if (require.main === module) {
  startServer();
}

module.exports = { startServer };

This separation makes it easier to import the app for testing without starting the server.

Testing Configuration

// src/config.js
module.exports = {
  port: process.env.PORT || 3000,
  environment: process.env.NODE_ENV || 'development',
  db: {
    url: process.env.NODE_ENV === 'test'
      ? process.env.TEST_DB_URL || 'mongodb://localhost:27017/test-db'
      : process.env.DB_URL || 'mongodb://localhost:27017/app-db',
    options: {
      useNewUrlParser: true,
      useUnifiedTopology: true
    }
  },
  jwt: {
    secret: process.env.JWT_SECRET || 'dev-secret',
    expiresIn: process.env.JWT_EXPIRES_IN || '1d'
  }
};
// test/setup.js
process.env.NODE_ENV = 'test';
process.env.TEST_DB_URL = 'mongodb://localhost:27017/test-db';
process.env.JWT_SECRET = 'test-secret';

const mongoose = require('mongoose');
const { MongoMemoryServer } = require('mongodb-memory-server');

let mongoServer;

// Setup before tests run
beforeAll(async () => {
  mongoServer = await MongoMemoryServer.create();
  const mongoUri = mongoServer.getUri();
  process.env.TEST_DB_URL = mongoUri;
});

// Clean up after tests complete
afterAll(async () => {
  if (mongoose.connection.readyState !== 0) {
    await mongoose.disconnect();
  }
  if (mongoServer) {
    await mongoServer.stop();
  }
});

Test Database Utilities

// test/utils/db.js
const mongoose = require('mongoose');
const { User, Product, Order } = require('../../src/models');

async function connectDB() {
  if (mongoose.connection.readyState === 0) {
    await mongoose.connect(process.env.TEST_DB_URL, {
      useNewUrlParser: true,
      useUnifiedTopology: true
    });
  }
}

async function clearDatabase() {
  if (mongoose.connection.readyState !== 0) {
    const collections = mongoose.connection.collections;
    
    for (const key in collections) {
      const collection = collections[key];
      await collection.deleteMany({});
    }
  }
}

async function disconnectDB() {
  if (mongoose.connection.readyState !== 0) {
    await mongoose.disconnect();
  }
}

async function createTestUser(data = {}) {
  return await User.create({
    name: 'Test User',
    email: `test-${Math.random().toString(36).substring(2)}@example.com`,
    password: 'password123',
    ...data
  });
}

// Add more helper functions for other collections

module.exports = {
  connectDB,
  clearDatabase,
  disconnectDB,
  createTestUser
};

Testing Tools for API Endpoints

The right tools make API testing more efficient, just as a master chef relies on quality knives.

Supertest

A popular HTTP assertion library that allows testing Express.js applications.

// Example Supertest usage
const request = require('supertest');
const app = require('../src/app');

test('GET /api/users returns list of users', async () => {
  const response = await request(app)
    .get('/api/users')
    .set('Authorization', `Bearer ${token}`)
    .expect('Content-Type', /json/)
    .expect(200);
  
  expect(response.body).toBeInstanceOf(Array);
});

Jest

Testing framework that works well with Supertest for assertions and test organization.

// Example Jest test structure
describe('User API', () => {
  let token;
  
  beforeEach(async () => {
    // Set up test data
    const user = await createTestUser();
    token = generateToken(user);
  });
  
  describe('GET /api/users', () => {
    test('returns 401 without token', async () => {
      await request(app)
        .get('/api/users')
        .expect(401);
    });
    
    test('returns user list with valid token', async () => {
      const response = await request(app)
        .get('/api/users')
        .set('Authorization', `Bearer ${token}`)
        .expect(200);
      
      expect(response.body.length).toBeGreaterThan(0);
    });
  });
});

Postman/Newman

Postman provides a GUI for API testing and Newman allows running these tests from the command line.

// Example of running Postman tests via Newman
const newman = require('newman');

newman.run({
  collection: require('./postman/collection.json'),
  environment: require('./postman/environment.json'),
  reporters: ['cli', 'junit'],
  reporter: {
    junit: {
      export: './test-results/postman-results.xml'
    }
  }
}, function (err) {
  if (err) { throw err; }
  console.log('Collection run complete!');
});

REST Client (VS Code Extension)

A lightweight alternative that allows creating HTTP requests in a simple text file.

// Example .http file for REST Client
### Get all users
GET http://localhost:3000/api/users
Authorization: Bearer {{$token}}

### Create new user
POST http://localhost:3000/api/users
Content-Type: application/json

{
  "name": "New User",
  "email": "newuser@example.com",
  "password": "password123"
}

Pactum.js

A newer API testing library that offers a more readable syntax.

const { spec } = require('pactum');

it('should get user by id', async () => {
  await spec()
    .get('/api/users/{id}')
    .withPathParams('id', 1)
    .withHeaders('Authorization', 'Bearer ' + token)
    .expectStatus(200)
    .expectJsonSchema({
      type: 'object',
      properties: {
        id: { type: 'number' },
        name: { type: 'string' }
      }
    });
});

Choosing the Right Tool

Your choice depends on your specific needs:

  • Supertest + Jest: Great for developers who want tests alongside code
  • Postman/Newman: Excellent for teams with non-developers who need to test APIs
  • REST Client: Perfect for quick ad-hoc testing during development
  • Pactum.js: Good for scenarios requiring complex assertions and validations

Writing Comprehensive API Tests

Basic CRUD Endpoint Testing

Let's test a set of API endpoints for a product resource:

// test/api/products.test.js
const request = require('supertest');
const app = require('../../src/app');
const db = require('../utils/db');
const { createTestUser } = require('../utils/auth');

describe('Products API', () => {
  let adminToken, userToken;
  let testProduct;
  
  beforeAll(async () => {
    await db.connectDB();
  });
  
  afterAll(async () => {
    await db.disconnectDB();
  });
  
  beforeEach(async () => {
    await db.clearDatabase();
    
    // Create test users
    const admin = await createTestUser({ role: 'admin' });
    const user = await createTestUser({ role: 'user' });
    
    adminToken = admin.generateToken();
    userToken = user.generateToken();
    
    // Create a test product
    testProduct = await request(app)
      .post('/api/products')
      .set('Authorization', `Bearer ${adminToken}`)
      .send({
        name: 'Test Product',
        description: 'A product for testing',
        price: 99.99,
        sku: 'TEST-1234',
        category: 'electronics'
      });
  });
  
  describe('GET /api/products', () => {
    test('returns list of products', async () => {
      const response = await request(app)
        .get('/api/products')
        .expect('Content-Type', /json/)
        .expect(200);
      
      expect(response.body).toBeInstanceOf(Array);
      expect(response.body.length).toBeGreaterThan(0);
      expect(response.body[0]).toHaveProperty('name');
      expect(response.body[0]).toHaveProperty('price');
    });
    
    test('supports filtering by category', async () => {
      // Create a product in different category
      await request(app)
        .post('/api/products')
        .set('Authorization', `Bearer ${adminToken}`)
        .send({
          name: 'Another Product',
          description: 'In a different category',
          price: 49.99,
          sku: 'TEST-5678',
          category: 'books'
        });
      
      const response = await request(app)
        .get('/api/products?category=electronics')
        .expect(200);
      
      expect(response.body.length).toBe(1);
      expect(response.body[0].category).toBe('electronics');
    });
    
    test('supports pagination', async () => {
      // Create 10 more products
      for (let i = 0; i < 10; i++) {
        await request(app)
          .post('/api/products')
          .set('Authorization', `Bearer ${adminToken}`)
          .send({
            name: `Product ${i}`,
            description: `Description ${i}`,
            price: 10 + i,
            sku: `SKU-${i}`,
            category: 'test'
          });
      }
      
      const response = await request(app)
        .get('/api/products?page=1&limit=5')
        .expect(200);
      
      expect(response.body.length).toBe(5);
      expect(response.header).toHaveProperty('x-total-count');
      expect(parseInt(response.header['x-total-count'])).toBeGreaterThan(5);
    });
  });
  
  describe('GET /api/products/:id', () => {
    test('returns product by id', async () => {
      const response = await request(app)
        .get(`/api/products/${testProduct.body.id}`)
        .expect('Content-Type', /json/)
        .expect(200);
      
      expect(response.body.id).toBe(testProduct.body.id);
      expect(response.body.name).toBe('Test Product');
      expect(response.body.price).toBe(99.99);
    });
    
    test('returns 404 for non-existent product', async () => {
      await request(app)
        .get('/api/products/nonexistentid')
        .expect(404);
    });
  });
  
  describe('POST /api/products', () => {
    test('admin can create a product', async () => {
      const newProduct = {
        name: 'New Product',
        description: 'Brand new product',
        price: 129.99,
        sku: 'NEW-1234',
        category: 'electronics'
      };
      
      const response = await request(app)
        .post('/api/products')
        .set('Authorization', `Bearer ${adminToken}`)
        .send(newProduct)
        .expect('Content-Type', /json/)
        .expect(201);
      
      expect(response.body.id).toBeDefined();
      expect(response.body.name).toBe(newProduct.name);
      expect(response.body.price).toBe(newProduct.price);
      
      // Verify product was actually created in the database
      const getResponse = await request(app)
        .get(`/api/products/${response.body.id}`)
        .expect(200);
      
      expect(getResponse.body.name).toBe(newProduct.name);
    });
    
    test('regular user cannot create a product', async () => {
      const newProduct = {
        name: 'Unauthorized Product',
        description: 'Should not be created',
        price: 9.99,
        sku: 'UNAUTH-1234',
        category: 'electronics'
      };
      
      await request(app)
        .post('/api/products')
        .set('Authorization', `Bearer ${userToken}`)
        .send(newProduct)
        .expect(403);
    });
    
    test('returns 400 for invalid product data', async () => {
      // Missing required fields
      const invalidProduct = {
        name: 'Invalid Product'
        // Missing price, sku, etc.
      };
      
      const response = await request(app)
        .post('/api/products')
        .set('Authorization', `Bearer ${adminToken}`)
        .send(invalidProduct)
        .expect('Content-Type', /json/)
        .expect(400);
      
      expect(response.body).toHaveProperty('error');
    });
  });
  
  describe('PUT /api/products/:id', () => {
    test('admin can update a product', async () => {
      const updates = {
        name: 'Updated Product Name',
        price: 149.99
      };
      
      const response = await request(app)
        .put(`/api/products/${testProduct.body.id}`)
        .set('Authorization', `Bearer ${adminToken}`)
        .send(updates)
        .expect('Content-Type', /json/)
        .expect(200);
      
      expect(response.body.name).toBe(updates.name);
      expect(response.body.price).toBe(updates.price);
      expect(response.body.description).toBe(testProduct.body.description); // Unchanged
      
      // Verify the update persisted
      const getResponse = await request(app)
        .get(`/api/products/${testProduct.body.id}`)
        .expect(200);
      
      expect(getResponse.body.name).toBe(updates.name);
    });
    
    test('regular user cannot update a product', async () => {
      await request(app)
        .put(`/api/products/${testProduct.body.id}`)
        .set('Authorization', `Bearer ${userToken}`)
        .send({ price: 0.99 })
        .expect(403);
      
      // Verify product was not changed
      const getResponse = await request(app)
        .get(`/api/products/${testProduct.body.id}`)
        .expect(200);
      
      expect(getResponse.body.price).toBe(testProduct.body.price);
    });
    
    test('returns 404 for non-existent product', async () => {
      await request(app)
        .put('/api/products/nonexistentid')
        .set('Authorization', `Bearer ${adminToken}`)
        .send({ name: 'New Name' })
        .expect(404);
    });
  });
  
  describe('DELETE /api/products/:id', () => {
    test('admin can delete a product', async () => {
      await request(app)
        .delete(`/api/products/${testProduct.body.id}`)
        .set('Authorization', `Bearer ${adminToken}`)
        .expect(204);
      
      // Verify product was deleted
      await request(app)
        .get(`/api/products/${testProduct.body.id}`)
        .expect(404);
    });
    
    test('regular user cannot delete a product', async () => {
      await request(app)
        .delete(`/api/products/${testProduct.body.id}`)
        .set('Authorization', `Bearer ${userToken}`)
        .expect(403);
      
      // Verify product still exists
      await request(app)
        .get(`/api/products/${testProduct.body.id}`)
        .expect(200);
    });
    
    test('returns 404 for non-existent product', async () => {
      await request(app)
        .delete('/api/products/nonexistentid')
        .set('Authorization', `Bearer ${adminToken}`)
        .expect(404);
    });
  });
});

Testing Authentication and Authorization

// test/api/auth.test.js
const request = require('supertest');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
const app = require('../../src/app');
const db = require('../utils/db');
const config = require('../../src/config');

describe('Authentication API', () => {
  beforeAll(async () => {
    await db.connectDB();
  });
  
  afterAll(async () => {
    await db.disconnectDB();
  });
  
  beforeEach(async () => {
    await db.clearDatabase();
  });
  
  describe('POST /api/auth/register', () => {
    test('registers a new user', async () => {
      const userData = {
        name: 'Test User',
        email: 'test@example.com',
        password: 'Password123!'
      };
      
      const response = await request(app)
        .post('/api/auth/register')
        .send(userData)
        .expect('Content-Type', /json/)
        .expect(201);
      
      expect(response.body.user).toBeDefined();
      expect(response.body.user.name).toBe(userData.name);
      expect(response.body.user.email).toBe(userData.email);
      expect(response.body.token).toBeDefined();
      
      // Verify token is valid
      const decoded = jwt.verify(response.body.token, config.jwt.secret);
      expect(decoded.id).toBe(response.body.user.id);
      expect(decoded.email).toBe(userData.email);
      
      // Password should not be returned
      expect(response.body.user.password).toBeUndefined();
      
      // Verify user was saved to database
      const user = await db.findUserByEmail(userData.email);
      expect(user).toBeTruthy();
      
      // Password should be hashed
      const passwordMatch = await bcrypt.compare(userData.password, user.password);
      expect(passwordMatch).toBe(true);
    });
    
    test('prevents duplicate email registration', async () => {
      // Create a user first
      const userData = {
        name: 'Original User',
        email: 'duplicate@example.com',
        password: 'Password123!'
      };
      
      await request(app)
        .post('/api/auth/register')
        .send(userData);
      
      // Try to register with the same email
      const duplicateResponse = await request(app)
        .post('/api/auth/register')
        .send({
          name: 'Duplicate User',
          email: 'duplicate@example.com',
          password: 'DifferentPass123!'
        })
        .expect('Content-Type', /json/)
        .expect(400);
      
      expect(duplicateResponse.body.error).toBeDefined();
      expect(duplicateResponse.body.error).toContain('email already in use');
    });
    
    test('validates password requirements', async () => {
      const response = await request(app)
        .post('/api/auth/register')
        .send({
          name: 'Test User',
          email: 'test@example.com',
          password: 'weak'
        })
        .expect(400);
      
      expect(response.body.error).toBeDefined();
    });
  });
  
  describe('POST /api/auth/login', () => {
    beforeEach(async () => {
      // Create a test user
      const hashedPassword = await bcrypt.hash('Password123!', 10);
      await db.createUser({
        name: 'Test User',
        email: 'test@example.com',
        password: hashedPassword
      });
    });
    
    test('logs in with valid credentials', async () => {
      const response = await request(app)
        .post('/api/auth/login')
        .send({
          email: 'test@example.com',
          password: 'Password123!'
        })
        .expect('Content-Type', /json/)
        .expect(200);
      
      expect(response.body.user).toBeDefined();
      expect(response.body.user.email).toBe('test@example.com');
      expect(response.body.token).toBeDefined();
    });
    
    test('rejects invalid password', async () => {
      const response = await request(app)
        .post('/api/auth/login')
        .send({
          email: 'test@example.com',
          password: 'WrongPassword123!'
        })
        .expect(401);
      
      expect(response.body.error).toBeDefined();
      expect(response.body.token).toBeUndefined();
    });
    
    test('rejects non-existent user', async () => {
      const response = await request(app)
        .post('/api/auth/login')
        .send({
          email: 'nonexistent@example.com',
          password: 'Password123!'
        })
        .expect(401);
      
      expect(response.body.error).toBeDefined();
    });
  });
  
  describe('GET /api/auth/me', () => {
    let token;
    let userId;
    
    beforeEach(async () => {
      // Create a test user
      const hashedPassword = await bcrypt.hash('Password123!', 10);
      const user = await db.createUser({
        name: 'Test User',
        email: 'test@example.com',
        password: hashedPassword
      });
      
      userId = user.id;
      
      // Generate a token
      token = jwt.sign(
        { id: user.id, email: user.email },
        config.jwt.secret,
        { expiresIn: config.jwt.expiresIn }
      );
    });
    
    test('returns user profile with valid token', async () => {
      const response = await request(app)
        .get('/api/auth/me')
        .set('Authorization', `Bearer ${token}`)
        .expect('Content-Type', /json/)
        .expect(200);
      
      expect(response.body.id).toBe(userId);
      expect(response.body.name).toBe('Test User');
      expect(response.body.email).toBe('test@example.com');
      expect(response.body.password).toBeUndefined(); // Password should not be returned
    });
    
    test('rejects request without token', async () => {
      await request(app)
        .get('/api/auth/me')
        .expect(401);
    });
    
    test('rejects request with invalid token', async () => {
      await request(app)
        .get('/api/auth/me')
        .set('Authorization', 'Bearer invalidtoken')
        .expect(401);
    });
    
    test('rejects request with expired token', async () => {
      // Create an expired token
      const expiredToken = jwt.sign(
        { id: userId, email: 'test@example.com' },
        config.jwt.secret,
        { expiresIn: '0s' }
      );
      
      await request(app)
        .get('/api/auth/me')
        .set('Authorization', `Bearer ${expiredToken}`)
        .expect(401);
    });
  });
});

Testing Complex Workflows

// test/api/order-workflow.test.js
const request = require('supertest');
const app = require('../../src/app');
const db = require('../utils/db');
const { createTestUser } = require('../utils/auth');

describe('Order Workflow', () => {
  let userToken;
  let userId;
  let products = [];
  
  beforeAll(async () => {
    await db.connectDB();
  });
  
  afterAll(async () => {
    await db.disconnectDB();
  });
  
  beforeEach(async () => {
    await db.clearDatabase();
    
    // Create a test user
    const user = await createTestUser();
    userId = user.id;
    userToken = user.generateToken();
    
    // Create test products
    const productData = [
      { name: 'Product 1', price: 10.99, sku: 'SKU001', inventory: 100 },
      { name: 'Product 2', price: 24.99, sku: 'SKU002', inventory: 50 },
      { name: 'Product 3', price: 5.99, sku: 'SKU003', inventory: 0 } // Out of stock
    ];
    
    const adminToken = (await createTestUser({ role: 'admin' })).generateToken();
    
    products = [];
    for (const data of productData) {
      const response = await request(app)
        .post('/api/products')
        .set('Authorization', `Bearer ${adminToken}`)
        .send(data);
      
      products.push(response.body);
    }
  });
  
  test('complete order flow - add to cart, checkout, verify inventory, view order', async () => {
    // Step 1: Add items to cart
    const cart = await request(app)
      .post('/api/cart')
      .set('Authorization', `Bearer ${userToken}`)
      .send({
        productId: products[0].id,
        quantity: 2
      })
      .expect(200);
    
    expect(cart.body.items).toHaveLength(1);
    expect(cart.body.items[0].productId).toBe(products[0].id);
    expect(cart.body.items[0].quantity).toBe(2);
    
    // Add another item
    await request(app)
      .post('/api/cart')
      .set('Authorization', `Bearer ${userToken}`)
      .send({
        productId: products[1].id,
        quantity: 1
      })
      .expect(200);
    
    // Step 2: View cart
    const cartResponse = await request(app)
      .get('/api/cart')
      .set('Authorization', `Bearer ${userToken}`)
      .expect(200);
    
    expect(cartResponse.body.items).toHaveLength(2);
    expect(cartResponse.body.total).toBe((products[0].price * 2) + products[1].price);
    
    // Step 3: Try to add out-of-stock item
    const outOfStockResponse = await request(app)
      .post('/api/cart')
      .set('Authorization', `Bearer ${userToken}`)
      .send({
        productId: products[2].id, // This is out of stock
        quantity: 1
      })
      .expect(400);
    
    expect(outOfStockResponse.body.error).toContain('out of stock');
    
    // Step 4: Create order from cart
    const orderResponse = await request(app)
      .post('/api/orders')
      .set('Authorization', `Bearer ${userToken}`)
      .send({
        shippingAddress: {
          street: '123 Test St',
          city: 'Test City',
          state: 'TS',
          zipCode: '12345',
          country: 'Test Country'
        },
        paymentMethod: 'credit_card',
        paymentDetails: {
          cardNumber: '4111111111111111',
          expiryMonth: 12,
          expiryYear: 2030,
          cvv: '123'
        }
      })
      .expect(201);
    
    expect(orderResponse.body.id).toBeDefined();
    expect(orderResponse.body.status).toBe('created');
    expect(orderResponse.body.items).toHaveLength(2);
    expect(orderResponse.body.total).toBe(cartResponse.body.total);
    
    // Step 5: Verify cart is now empty
    const emptyCartResponse = await request(app)
      .get('/api/cart')
      .set('Authorization', `Bearer ${userToken}`)
      .expect(200);
    
    expect(emptyCartResponse.body.items).toHaveLength(0);
    
    // Step 6: Verify inventory was updated
    const product1Response = await request(app)
      .get(`/api/products/${products[0].id}`)
      .expect(200);
    
    expect(product1Response.body.inventory).toBe(products[0].inventory - 2);
    
    // Step 7: View order details
    const orderId = orderResponse.body.id;
    const orderDetailsResponse = await request(app)
      .get(`/api/orders/${orderId}`)
      .set('Authorization', `Bearer ${userToken}`)
      .expect(200);
    
    expect(orderDetailsResponse.body.id).toBe(orderId);
    expect(orderDetailsResponse.body.user.id).toBe(userId);
    expect(orderDetailsResponse.body.status).toBe('created');
    
    // Step 8: List all user orders
    const ordersListResponse = await request(app)
      .get('/api/orders')
      .set('Authorization', `Bearer ${userToken}`)
      .expect(200);
    
    expect(ordersListResponse.body).toBeInstanceOf(Array);
    expect(ordersListResponse.body.length).toBeGreaterThan(0);
    expect(ordersListResponse.body[0].id).toBe(orderId);
  });
  
  test('handles payment failure gracefully', async () => {
    // Step 1: Add item to cart
    await request(app)
      .post('/api/cart')
      .set('Authorization', `Bearer ${userToken}`)
      .send({
        productId: products[0].id,
        quantity: 1
      })
      .expect(200);
    
    // Step 2: Create order with invalid payment details
    const orderResponse = await request(app)
      .post('/api/orders')
      .set('Authorization', `Bearer ${userToken}`)
      .send({
        shippingAddress: {
          street: '123 Test St',
          city: 'Test City',
          state: 'TS',
          zipCode: '12345',
          country: 'Test Country'
        },
        paymentMethod: 'credit_card',
        paymentDetails: {
          cardNumber: '4111111111111111',
          expiryMonth: 1,
          expiryYear: 2020, // Expired card
          cvv: '123'
        }
      })
      .expect(400);
    
    expect(orderResponse.body.error).toBeDefined();
    expect(orderResponse.body.error).toContain('payment');
    
    // Step 3: Verify inventory was not affected
    const productResponse = await request(app)
      .get(`/api/products/${products[0].id}`)
      .expect(200);
    
    expect(productResponse.body.inventory).toBe(products[0].inventory);
    
    // Step 4: Verify cart still contains items
    const cartResponse = await request(app)
      .get('/api/cart')
      .set('Authorization', `Bearer ${userToken}`)
      .expect(200);
    
    expect(cartResponse.body.items).toHaveLength(1);
  });
});

Testing API Response Schemas

Schema validation ensures your API response structure remains consistent and follows your documentation.

Using JSON Schema Validation

// test/schemas/product.schema.js
const productSchema = {
  type: 'object',
  required: ['id', 'name', 'price', 'sku', 'inventory'],
  properties: {
    id: { type: 'string' },
    name: { type: 'string' },
    description: { type: ['string', 'null'] },
    price: { type: 'number', minimum: 0 },
    sku: { type: 'string' },
    category: { type: 'string' },
    inventory: { type: 'number', minimum: 0 },
    createdAt: { type: 'string', format: 'date-time' },
    updatedAt: { type: 'string', format: 'date-time' }
  },
  additionalProperties: false
};

const productListSchema = {
  type: 'array',
  items: productSchema
};

module.exports = {
  productSchema,
  productListSchema
};
// test/api/product-schema.test.js
const request = require('supertest');
const Ajv = require('ajv');
const addFormats = require('ajv-formats');
const app = require('../../src/app');
const db = require('../utils/db');
const { createTestUser } = require('../utils/auth');
const { productSchema, productListSchema } = require('../schemas/product.schema');

describe('Product API Schema Validation', () => {
  let adminToken;
  let testProduct;
  
  // Set up Ajv (JSON Schema validator)
  const ajv = new Ajv({ allErrors: true });
  addFormats(ajv);
  const validateProduct = ajv.compile(productSchema);
  const validateProductList = ajv.compile(productListSchema);
  
  beforeAll(async () => {
    await db.connectDB();
  });
  
  afterAll(async () => {
    await db.disconnectDB();
  });
  
  beforeEach(async () => {
    await db.clearDatabase();
    
    // Create admin user
    const admin = await createTestUser({ role: 'admin' });
    adminToken = admin.generateToken();
    
    // Create test product
    const response = await request(app)
      .post('/api/products')
      .set('Authorization', `Bearer ${adminToken}`)
      .send({
        name: 'Test Product',
        description: 'Product for schema testing',
        price: 99.99,
        sku: 'SCHEMA-TEST',
        category: 'test',
        inventory: 100
      });
    
    testProduct = response.body;
  });
  
  test('GET /api/products returns data matching product list schema', async () => {
    const response = await request(app)
      .get('/api/products')
      .expect(200);
    
    const valid = validateProductList(response.body);
    
    if (!valid) {
      console.error('Schema validation errors:', ajv.errorsText(validateProductList.errors));
    }
    
    expect(valid).toBe(true);
  });
  
  test('GET /api/products/:id returns data matching product schema', async () => {
    const response = await request(app)
      .get(`/api/products/${testProduct.id}`)
      .expect(200);
    
    const valid = validateProduct(response.body);
    
    if (!valid) {
      console.error('Schema validation errors:', ajv.errorsText(validateProduct.errors));
    }
    
    expect(valid).toBe(true);
  });
  
  test('POST /api/products returns data matching product schema', async () => {
    const newProduct = {
      name: 'New Schema Product',
      description: 'Testing schema for new products',
      price: 49.99,
      sku: 'SCHEMA-NEW',
      category: 'test',
      inventory: 50
    };
    
    const response = await request(app)
      .post('/api/products')
      .set('Authorization', `Bearer ${adminToken}`)
      .send(newProduct)
      .expect(201);
    
    const valid = validateProduct(response.body);
    
    if (!valid) {
      console.error('Schema validation errors:', ajv.errorsText(validateProduct.errors));
    }
    
    expect(valid).toBe(true);
    
    // Verify all sent fields are in the response
    expect(response.body.name).toBe(newProduct.name);
    expect(response.body.price).toBe(newProduct.price);
    expect(response.body.sku).toBe(newProduct.sku);
  });
  
  test('PUT /api/products/:id returns data matching product schema', async () => {
    const updates = {
      name: 'Updated Schema Product',
      price: 129.99
    };
    
    const response = await request(app)
      .put(`/api/products/${testProduct.id}`)
      .set('Authorization', `Bearer ${adminToken}`)
      .send(updates)
      .expect(200);
    
    const valid = validateProduct(response.body);
    
    if (!valid) {
      console.error('Schema validation errors:', ajv.errorsText(validateProduct.errors));
    }
    
    expect(valid).toBe(true);
    
    // Verify updates were applied
    expect(response.body.name).toBe(updates.name);
    expect(response.body.price).toBe(updates.price);
    
    // Other fields should remain unchanged
    expect(response.body.description).toBe(testProduct.description);
    expect(response.body.sku).toBe(testProduct.sku);
  });
});

Using OpenAPI/Swagger for Schema Validation

If your API is documented with OpenAPI/Swagger, you can use those definitions for validation:

// test/utils/openapi-validator.js
const OpenAPIResponseValidator = require('openapi-response-validator').default;
const swaggerDocument = require('../../api-docs/swagger.json');

function createResponseValidator(path, method) {
  const apiPath = swaggerDocument.paths[path];
  if (!apiPath) {
    throw new Error(`Path ${path} not found in Swagger document`);
  }
  
  const apiMethod = apiPath[method.toLowerCase()];
  if (!apiMethod) {
    throw new Error(`Method ${method} not found for path ${path} in Swagger document`);
  }
  
  return new OpenAPIResponseValidator({
    responses: apiMethod.responses,
    components: swaggerDocument.components
  });
}

function validateResponse(validator, statusCode, response) {
  statusCode = statusCode.toString();
  const validationError = validator.validateResponse(statusCode, response);
  
  return {
    valid: !validationError,
    error: validationError
  };
}

module.exports = {
  createResponseValidator,
  validateResponse
};
// test/api/openapi-validation.test.js
const request = require('supertest');
const app = require('../../src/app');
const { createResponseValidator, validateResponse } = require('../utils/openapi-validator');
const db = require('../utils/db');
const { createTestUser } = require('../utils/auth');

describe('OpenAPI Schema Validation', () => {
  let productValidator;
  let userValidator;
  let adminToken;
  let testProduct;
  
  beforeAll(async () => {
    await db.connectDB();
    
    // Create validators based on OpenAPI spec
    productValidator = {
      getAll: createResponseValidator('/api/products', 'GET'),
      getById: createResponseValidator('/api/products/{id}', 'GET'),
      create: createResponseValidator('/api/products', 'POST'),
      update: createResponseValidator('/api/products/{id}', 'PUT')
    };
    
    userValidator = {
      register: createResponseValidator('/api/auth/register', 'POST'),
      login: createResponseValidator('/api/auth/login', 'POST'),
      getProfile: createResponseValidator('/api/auth/me', 'GET')
    };
  });
  
  afterAll(async () => {
    await db.disconnectDB();
  });
  
  beforeEach(async () => {
    await db.clearDatabase();
    
    // Create admin user
    const admin = await createTestUser({ role: 'admin' });
    adminToken = admin.generateToken();
    
    // Create test product
    const response = await request(app)
      .post('/api/products')
      .set('Authorization', `Bearer ${adminToken}`)
      .send({
        name: 'OpenAPI Test Product',
        description: 'Product for OpenAPI schema testing',
        price: 88.99,
        sku: 'OPENAPI-TEST',
        category: 'test',
        inventory: 75
      });
    
    testProduct = response.body;
  });
  
  describe('Product API OpenAPI Validation', () => {
    test('GET /api/products response matches OpenAPI schema', async () => {
      const response = await request(app)
        .get('/api/products')
        .expect(200);
      
      const validation = validateResponse(productValidator.getAll, 200, response.body);
      
      if (!validation.valid) {
        console.error('Validation error:', validation.error);
      }
      
      expect(validation.valid).toBe(true);
    });
    
    test('GET /api/products/:id response matches OpenAPI schema', async () => {
      const response = await request(app)
        .get(`/api/products/${testProduct.id}`)
        .expect(200);
      
      const validation = validateResponse(productValidator.getById, 200, response.body);
      expect(validation.valid).toBe(true);
    });
    
    test('POST /api/products response matches OpenAPI schema', async () => {
      const newProduct = {
        name: 'New OpenAPI Product',
        description: 'Testing OpenAPI schema for new products',
        price: 39.99,
        sku: 'OPENAPI-NEW',
        category: 'test',
        inventory: 25
      };
      
      const response = await request(app)
        .post('/api/products')
        .set('Authorization', `Bearer ${adminToken}`)
        .send(newProduct)
        .expect(201);
      
      const validation = validateResponse(productValidator.create, 201, response.body);
      expect(validation.valid).toBe(true);
    });
    
    test('PUT /api/products/:id response matches OpenAPI schema', async () => {
      const updates = {
        name: 'Updated OpenAPI Product',
        price: 149.99
      };
      
      const response = await request(app)
        .put(`/api/products/${testProduct.id}`)
        .set('Authorization', `Bearer ${adminToken}`)
        .send(updates)
        .expect(200);
      
      const validation = validateResponse(productValidator.update, 200, response.body);
      expect(validation.valid).toBe(true);
    });
  });
  
  describe('Auth API OpenAPI Validation', () => {
    test('POST /api/auth/register response matches OpenAPI schema', async () => {
      const userData = {
        name: 'OpenAPI Test User',
        email: 'openapi-test@example.com',
        password: 'Password123!'
      };
      
      const response = await request(app)
        .post('/api/auth/register')
        .send(userData)
        .expect(201);
      
      const validation = validateResponse(userValidator.register, 201, response.body);
      expect(validation.valid).toBe(true);
    });
    
    test('POST /api/auth/login response matches OpenAPI schema', async () => {
      // Create a user first
      const userData = {
        name: 'Login Test User',
        email: 'login-test@example.com',
        password: 'Password123!'
      };
      
      await request(app)
        .post('/api/auth/register')
        .send(userData);
      
      // Test login
      const response = await request(app)
        .post('/api/auth/login')
        .send({
          email: userData.email,
          password: userData.password
        })
        .expect(200);
      
      const validation = validateResponse(userValidator.login, 200, response.body);
      expect(validation.valid).toBe(true);
    });
    
    test('GET /api/auth/me response matches OpenAPI schema', async () => {
      // Create a user
      const user = await createTestUser();
      const token = user.generateToken();
      
      const response = await request(app)
        .get('/api/auth/me')
        .set('Authorization', `Bearer ${token}`)
        .expect(200);
      
      const validation = validateResponse(userValidator.getProfile, 200, response.body);
      expect(validation.valid).toBe(true);
    });
  });
});

Testing Error Handling

Robust APIs must handle errors gracefully and return appropriate error responses.

Common Error Scenarios to Test

// test/api/error-handling.test.js
const request = require('supertest');
const nock = require('nock');
const app = require('../../src/app');
const db = require('../utils/db');
const { createTestUser } = require('../utils/auth');

describe('API Error Handling', () => {
  let userToken, adminToken;
  
  beforeAll(async () => {
    await db.connectDB();
  });
  
  afterAll(async () => {
    await db.disconnectDB();
    nock.cleanAll();
  });
  
  beforeEach(async () => {
    await db.clearDatabase();
    
    // Create test users
    const user = await createTestUser({ role: 'user' });
    userToken = user.generateToken();
    
    const admin = await createTestUser({ role: 'admin' });
    adminToken = admin.generateToken();
  });
  
  describe('Validation Errors', () => {
    test('returns 400 for missing required fields', async () => {
      const response = await request(app)
        .post('/api/products')
        .set('Authorization', `Bearer ${adminToken}`)
        .send({
          name: 'Incomplete Product'
          // Missing required fields
        })
        .expect(400);
      
      expect(response.body.error).toBeDefined();
      expect(response.body.error).toContain('required');
      // Should specify which fields are missing
      expect(response.body.details).toBeDefined();
    });
    
    test('returns 400 for invalid data types', async () => {
      const response = await request(app)
        .post('/api/products')
        .set('Authorization', `Bearer ${adminToken}`)
        .send({
          name: 'Invalid Product',
          description: 'Invalid data types test',
          price: 'not-a-number', // Should be a number
          sku: 123, // Should be a string
          inventory: 50
        })
        .expect(400);
      
      expect(response.body.error).toBeDefined();
      expect(response.body.details).toBeDefined();
      expect(response.body.details).toContain('price');
    });
    
    test('returns 400 for values outside allowed ranges', async () => {
      const response = await request(app)
        .post('/api/products')
        .set('Authorization', `Bearer ${adminToken}`)
        .send({
          name: 'Invalid Range Product',
          description: 'Testing range validation',
          price: -10, // Price should be positive
          sku: 'RANGE-TEST',
          inventory: -5 // Inventory should be positive
        })
        .expect(400);
      
      expect(response.body.error).toBeDefined();
      expect(response.body.details).toBeDefined();
      expect(response.body.details).toContain('positive');
    });
  });
  
  describe('Authentication Errors', () => {
    test('returns 401 for missing token', async () => {
      await request(app)
        .get('/api/auth/me')
        .expect(401);
    });
    
    test('returns 401 for invalid token format', async () => {
      await request(app)
        .get('/api/auth/me')
        .set('Authorization', 'not-a-valid-token-format')
        .expect(401);
    });
    
    test('returns 401 for expired token', async () => {
      // Create an expired token (would need to mock JWT verification)
      const expiredToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjEyMyIsImVtYWlsIjoidGVzdEBleGFtcGxlLmNvbSIsImlhdCI6MTUxNjIzOTAyMiwiZXhwIjoxNTE2MjM5MDIyfQ.signature';
      
      await request(app)
        .get('/api/auth/me')
        .set('Authorization', `Bearer ${expiredToken}`)
        .expect(401);
    });
  });
  
  describe('Authorization Errors', () => {
    test('returns 403 when non-admin tries to create product', async () => {
      await request(app)
        .post('/api/products')
        .set('Authorization', `Bearer ${userToken}`) // Regular user token
        .send({
          name: 'Unauthorized Product',
          description: 'Should not be created',
          price: 19.99,
          sku: 'UNAUTH-TEST',
          inventory: 10
        })
        .expect(403);
    });
    
    test('returns 403 when user tries to access another user\'s data', async () => {
      // Create a second user
      const anotherUser = await createTestUser();
      
      // Create an order for the second user
      const orderResponse = await request(app)
        .post('/api/orders')
        .set('Authorization', `Bearer ${anotherUser.generateToken()}`)
        .send({
          items: [{ productId: 'someProductId', quantity: 1 }],
          shippingAddress: { street: '123 Test St', city: 'Test City' }
        });
      
      // Try to access it with first user's token
      await request(app)
        .get(`/api/orders/${orderResponse.body.id}`)
        .set('Authorization', `Bearer ${userToken}`)
        .expect(403);
    });
  });
  
  describe('Resource Not Found Errors', () => {
    test('returns 404 for non-existent product', async () => {
      await request(app)
        .get('/api/products/nonexistentid')
        .expect(404);
    });
    
    test('returns 404 for non-existent user', async () => {
      await request(app)
        .get('/api/users/nonexistentid')
        .set('Authorization', `Bearer ${adminToken}`)
        .expect(404);
    });
    
    test('returns 404 for non-existent order', async () => {
      await request(app)
        .get('/api/orders/nonexistentid')
        .set('Authorization', `Bearer ${userToken}`)
        .expect(404);
    });
  });
  
  describe('Conflict Errors', () => {
    test('returns 409 for duplicate email during registration', async () => {
      // Create a user
      const userData = {
        name: 'Original User',
        email: 'conflict-test@example.com',
        password: 'Password123!'
      };
      
      await request(app)
        .post('/api/auth/register')
        .send(userData);
      
      // Try to create another user with the same email
      const response = await request(app)
        .post('/api/auth/register')
        .send({
          name: 'Duplicate User',
          email: 'conflict-test@example.com', // Same email
          password: 'AnotherPassword123!'
        })
        .expect(409);
      
      expect(response.body.error).toBeDefined();
      expect(response.body.error).toContain('already exists');
    });
    
    test('returns 409 for duplicate SKU when creating product', async () => {
      // Create a product
      const productData = {
        name: 'Original Product',
        description: 'Testing conflict errors',
        price: 29.99,
        sku: 'CONFLICT-TEST',
        inventory: 20
      };
      
      await request(app)
        .post('/api/products')
        .set('Authorization', `Bearer ${adminToken}`)
        .send(productData);
      
      // Try to create another product with the same SKU
      const response = await request(app)
        .post('/api/products')
        .set('Authorization', `Bearer ${adminToken}`)
        .send({
          name: 'Duplicate SKU Product',
          description: 'Should conflict',
          price: 39.99,
          sku: 'CONFLICT-TEST', // Same SKU
          inventory: 30
        })
        .expect(409);
      
      expect(response.body.error).toBeDefined();
      expect(response.body.error).toContain('SKU already exists');
    });
  });
  
  describe('Dependency Errors', () => {
    test('handles payment service errors gracefully', async () => {
      // Mock the payment service to fail
      nock('https://api.payment-provider.com')
        .post('/v1/charges')
        .reply(500, { error: 'Payment service unavailable' });
      
      // Add product to cart
      await request(app)
        .post('/api/cart')
        .set('Authorization', `Bearer ${userToken}`)
        .send({
          productId: 'someProductId',
          quantity: 1
        });
      
      // Try to checkout
      const response = await request(app)
      .post('/api/orders')
      .set('Authorization', `Bearer ${userToken}`)
      .send({
        shippingAddress: {
          street: '123 Test St',
          city: 'Test City',
          state: 'TS',
          zipCode: '12345',
          country: 'Test Country'
        },
        paymentMethod: 'credit_card',
        paymentDetails: {
          cardNumber: '4111111111111111',
          expiryMonth: 12,
          expiryYear: 2030,
          cvv: '123'
        }
      })
      .expect(500);
    
    expect(response.body.error).toBeDefined();
    // Should provide a user-friendly error, not expose internal details
    expect(response.body.error).not.toContain('Payment service unavailable');
    expect(response.body.error).toContain('payment processing');
  });
  
  test('handles database errors gracefully', async () => {
    // Simulate a database connection error
    const originalFindById = db.findProductById;
    db.findProductById = jest.fn().mockRejectedValue(new Error('Database connection lost'));
    
    const response = await request(app)
      .get('/api/products/someid')
      .expect(500);
    
    expect(response.body.error).toBeDefined();
    expect(response.body.error).toContain('server error');
    // Should not expose internal error details
    expect(response.body.error).not.toContain('Database connection lost');
    
    // Restore the original function
    db.findProductById = originalFindById;
  });
});

describe('Rate Limiting', () => {
  test('returns 429 when rate limit is exceeded', async () => {
    // Make many requests in quick succession
    const requests = [];
    for (let i = 0; i < 20; i++) {
      requests.push(
        request(app)
          .get('/api/products')
          .set('X-Forwarded-For', '192.168.1.1') // Same IP address
      );
    }
    
    const responses = await Promise.all(requests);
    
    // At least one response should be rate limited
    const rateLimited = responses.some(response => response.status === 429);
    expect(rateLimited).toBe(true);
    
    // Check error response format
    const limitedResponse = responses.find(response => response.status === 429);
    expect(limitedResponse.body.error).toBeDefined();
    expect(limitedResponse.body.error).toContain('too many requests');
    expect(limitedResponse.headers['retry-after']).toBeDefined();
  });
});
});

Testing Error Response Format Consistency

// test/api/error-format.test.js
const request = require('supertest');
const app = require('../../src/app');
const db = require('../utils/db');
const { createTestUser } = require('../utils/auth');

describe('Error Response Format', () => {
let adminToken;

beforeAll(async () => {
  await db.connectDB();
});

afterAll(async () => {
  await db.disconnectDB();
});

beforeEach(async () => {
  await db.clearDatabase();
  
  // Create admin user
  const admin = await createTestUser({ role: 'admin' });
  adminToken = admin.generateToken();
});

test('validation errors use consistent format', async () => {
  const response = await request(app)
    .post('/api/products')
    .set('Authorization', `Bearer ${adminToken}`)
    .send({
      // Missing required fields
    })
    .expect(400);
  
  // Check response structure
  expect(response.body).toHaveProperty('error');
  expect(response.body).toHaveProperty('details');
  expect(response.body).toHaveProperty('status', 400);
  expect(response.body).toHaveProperty('timestamp');
  expect(typeof response.body.error).toBe('string');
  expect(Array.isArray(response.body.details)).toBe(true);
});

test('authentication errors use consistent format', async () => {
  const response = await request(app)
    .get('/api/auth/me')
    .expect(401);
  
  // Check response structure
  expect(response.body).toHaveProperty('error');
  expect(response.body).toHaveProperty('status', 401);
  expect(response.body).toHaveProperty('timestamp');
  expect(typeof response.body.error).toBe('string');
});

test('not found errors use consistent format', async () => {
  const response = await request(app)
    .get('/api/products/nonexistentid')
    .expect(404);
  
  // Check response structure
  expect(response.body).toHaveProperty('error');
  expect(response.body).toHaveProperty('status', 404);
  expect(response.body).toHaveProperty('timestamp');
  expect(typeof response.body.error).toBe('string');
});

test('server errors use consistent format', async () => {
  // Simulate server error by making the database throw an error
  const originalFindById = db.findProductById;
  db.findProductById = jest.fn().mockRejectedValue(new Error('Simulated server error'));
  
  const response = await request(app)
    .get('/api/products/someid')
    .expect(500);
  
  // Check response structure
  expect(response.body).toHaveProperty('error');
  expect(response.body).toHaveProperty('status', 500);
  expect(response.body).toHaveProperty('timestamp');
  expect(typeof response.body.error).toBe('string');
  
  // Restore the original function
  db.findProductById = originalFindById;
});
});

Testing Performance and Concurrency

Beyond functional correctness, it's important to test how your API performs under load and handles multiple concurrent requests.

Basic Load Testing with Artillery

// load-tests/artillery-config.yml
config:
target: "http://localhost:3000"
phases:
  - duration: 60
    arrivalRate: 5
    rampTo: 50
    name: "Warm up phase"
  - duration: 120
    arrivalRate: 50
    name: "Sustained load phase"
defaults:
  headers:
    Content-Type: "application/json"
    Accept: "application/json"

scenarios:
- name: "Browse products and view details"
  flow:
    - get:
        url: "/api/products"
        capture:
          - json: "$[0].id"
            as: "productId"
    - get:
        url: "/api/products/{{ productId }}"
    - think: 3
    - get:
        url: "/api/products?category=electronics"
    - think: 2
    
- name: "User authentication flow"
  weight: 2
  flow:
    - post:
        url: "/api/auth/register"
        json:
          name: "Load User {{ $randomString(10) }}"
          email: "load-test-{{ $randomString(8) }}@example.com"
          password: "Password123!"
        capture:
          - json: "token"
            as: "authToken"
    - think: 1
    - get:
        url: "/api/auth/me"
        headers:
          Authorization: "Bearer {{ authToken }}"
    - think: 2

Running this with Artillery provides insights into your API's performance under load.

Testing Race Conditions

// test/api/concurrency.test.js
const request = require('supertest');
const app = require('../../src/app');
const db = require('../utils/db');
const { createTestUser } = require('../utils/auth');

describe('Concurrency Tests', () => {
let userToken, productId;

beforeAll(async () => {
  await db.connectDB();
});

afterAll(async () => {
  await db.disconnectDB();
});

beforeEach(async () => {
  await db.clearDatabase();
  
  // Create test user
  const user = await createTestUser();
  userToken = user.generateToken();
  
  // Create test product with limited inventory
  const adminToken = (await createTestUser({ role: 'admin' })).generateToken();
  const productResponse = await request(app)
    .post('/api/products')
    .set('Authorization', `Bearer ${adminToken}`)
    .send({
      name: 'Limited Product',
      description: 'Only 5 available',
      price: 99.99,
      sku: 'LIMITED-SKU',
      inventory: 5
    });
  
  productId = productResponse.body.id;
});

test('handles concurrent inventory updates correctly', async () => {
  // Create multiple concurrent requests to add product to cart
  const requests = [];
  for (let i = 0; i < 10; i++) {
    requests.push(
      request(app)
        .post('/api/cart')
        .set('Authorization', `Bearer ${userToken}`)
        .send({
          productId,
          quantity: 1
        })
    );
  }
  
  // Execute all requests concurrently
  const responses = await Promise.all(requests);
  
  // Check responses
  const successResponses = responses.filter(r => r.status === 200);
  const errorResponses = responses.filter(r => r.status === 400);
  
  // Should have 5 successful requests (inventory = 5) and 5 failures
  expect(successResponses.length).toBe(5);
  expect(errorResponses.length).toBe(5);
  
  // All error responses should mention inventory
  for (const response of errorResponses) {
    expect(response.body.error).toContain('inventory');
  }
  
  // Verify final inventory is 0
  const productResponse = await request(app)
    .get(`/api/products/${productId}`)
    .expect(200);
  
  expect(productResponse.body.inventory).toBe(0);
});

test('handles concurrent order creation correctly', async () => {
  // Add product to cart
  await request(app)
    .post('/api/cart')
    .set('Authorization', `Bearer ${userToken}`)
    .send({
      productId,
      quantity: 3
    });
  
  // Create multiple concurrent requests to create orders from the same cart
  const orderRequests = [];
  for (let i = 0; i < 3; i++) {
    orderRequests.push(
      request(app)
        .post('/api/orders')
        .set('Authorization', `Bearer ${userToken}`)
        .send({
          shippingAddress: {
            street: '123 Test St',
            city: 'Test City',
            state: 'TS',
            zipCode: '12345',
            country: 'Test Country'
          },
          paymentMethod: 'credit_card',
          paymentDetails: {
            cardNumber: '4111111111111111',
            expiryMonth: 12,
            expiryYear: 2030,
            cvv: '123'
          }
        })
    );
  }
  
  // Execute all requests concurrently
  const responses = await Promise.all(orderRequests);
  
  // Should have only one successful order
  const successResponses = responses.filter(r => r.status === 201);
  expect(successResponses.length).toBe(1);
  
  // Verify cart is now empty
  const cartResponse = await request(app)
    .get('/api/cart')
    .set('Authorization', `Bearer ${userToken}`)
    .expect(200);
  
  expect(cartResponse.body.items).toHaveLength(0);
  
  // Verify only one order was created
  const ordersResponse = await request(app)
    .get('/api/orders')
    .set('Authorization', `Bearer ${userToken}`)
    .expect(200);
  
  expect(ordersResponse.body).toHaveLength(1);
});
});

Functional Testing for API Workflows

Functional tests go beyond testing individual endpoints to verify that complete business workflows work correctly.

// test/functional/checkout-workflow.test.js
const request = require('supertest');
const app = require('../../src/app');
const db = require('../utils/db');
const { createTestUser } = require('../utils/auth');

describe('Checkout Workflow', () => {
let userToken, productIds = [];

beforeAll(async () => {
  await db.connectDB();
});

afterAll(async () => {
  await db.disconnectDB();
});

beforeEach(async () => {
  await db.clearDatabase();
  
  // Create test user
  const user = await createTestUser();
  userToken = user.generateToken();
  
  // Create test products
  const adminToken = (await createTestUser({ role: 'admin' })).generateToken();
  
  const products = [
    {
      name: 'Product 1',
      description: 'First test product',
      price: 29.99,
      sku: 'FUNC-TEST-1',
      inventory: 100
    },
    {
      name: 'Product 2',
      description: 'Second test product',
      price: 49.99,
      sku: 'FUNC-TEST-2',
      inventory: 50
    }
  ];
  
  productIds = [];
  for (const product of products) {
    const response = await request(app)
      .post('/api/products')
      .set('Authorization', `Bearer ${adminToken}`)
      .send(product);
    
    productIds.push(response.body.id);
  }
});

test('complete checkout workflow from cart to order confirmation', async () => {
  // Step 1: Add products to cart
  await request(app)
    .post('/api/cart')
    .set('Authorization', `Bearer ${userToken}`)
    .send({
      productId: productIds[0],
      quantity: 2
    })
    .expect(200);
  
  await request(app)
    .post('/api/cart')
    .set('Authorization', `Bearer ${userToken}`)
    .send({
      productId: productIds[1],
      quantity: 1
    })
    .expect(200);
  
  // Step 2: View cart and calculate expected total
  const cartResponse = await request(app)
    .get('/api/cart')
    .set('Authorization', `Bearer ${userToken}`)
    .expect(200);
  
  expect(cartResponse.body.items).toHaveLength(2);
  
  const expectedTotal = (29.99 * 2) + 49.99;
  expect(cartResponse.body.total).toBeCloseTo(expectedTotal);
  
  // Step 3: Create a shipping address
  const addressResponse = await request(app)
    .post('/api/shipping-addresses')
    .set('Authorization', `Bearer ${userToken}`)
    .send({
      fullName: 'Test User',
      street: '123 Test Street',
      city: 'Test City',
      state: 'TS',
      zipCode: '12345',
      country: 'Test Country',
      isDefault: true
    })
    .expect(201);
  
  const addressId = addressResponse.body.id;
  
  // Step 4: Get shipping methods
  const shippingMethodsResponse = await request(app)
    .get('/api/shipping-methods')
    .expect(200);
  
  expect(shippingMethodsResponse.body).toBeInstanceOf(Array);
  expect(shippingMethodsResponse.body.length).toBeGreaterThan(0);
  
  const shippingMethod = shippingMethodsResponse.body[0];
  
  // Step 5: Create order
  const orderResponse = await request(app)
    .post('/api/orders')
    .set('Authorization', `Bearer ${userToken}`)
    .send({
      shippingAddressId: addressId,
      shippingMethodId: shippingMethod.id,
      paymentMethod: 'credit_card',
      paymentDetails: {
        cardNumber: '4111111111111111',
        expiryMonth: 12,
        expiryYear: 2030,
        cvv: '123'
      }
    })
    .expect(201);
  
  const orderId = orderResponse.body.id;
  expect(orderResponse.body.status).toBe('created');
  
  // Step 6: Verify cart is empty after order creation
  const emptyCartResponse = await request(app)
    .get('/api/cart')
    .set('Authorization', `Bearer ${userToken}`)
    .expect(200);
  
  expect(emptyCartResponse.body.items).toHaveLength(0);
  
  // Step 7: Verify order details
  const orderDetailsResponse = await request(app)
    .get(`/api/orders/${orderId}`)
    .set('Authorization', `Bearer ${userToken}`)
    .expect(200);
  
  expect(orderDetailsResponse.body.items).toHaveLength(2);
  expect(orderDetailsResponse.body.subtotal).toBeCloseTo(expectedTotal);
  expect(orderDetailsResponse.body.shippingAddress.id).toBe(addressId);
  expect(orderDetailsResponse.body.shippingMethod.id).toBe(shippingMethod.id);
  
  // Step 8: Simulate order payment confirmation (webhook callback)
  await request(app)
    .post('/api/webhooks/payment')
    .send({
      orderId,
      status: 'success',
      transactionId: 'test-transaction-123',
      amount: orderDetailsResponse.body.total
    })
    .expect(200);
  
  // Step 9: Verify order status is updated
  const updatedOrderResponse = await request(app)
    .get(`/api/orders/${orderId}`)
    .set('Authorization', `Bearer ${userToken}`)
    .expect(200);
  
  expect(updatedOrderResponse.body.status).toBe('paid');
  expect(updatedOrderResponse.body.paymentDetails.transactionId).toBe('test-transaction-123');
  
  // Step 10: Verify product inventory was updated
  const product1Response = await request(app)
    .get(`/api/products/${productIds[0]}`)
    .expect(200);
  
  expect(product1Response.body.inventory).toBe(98); // 100 - 2
  
  const product2Response = await request(app)
    .get(`/api/products/${productIds[1]}`)
    .expect(200);
  
  expect(product2Response.body.inventory).toBe(49); // 50 - 1
});

test('handles cart abandonment and restoration', async () => {
  // Step 1: Add products to cart
  await request(app)
    .post('/api/cart')
    .set('Authorization', `Bearer ${userToken}`)
    .send({
      productId: productIds[0],
      quantity: 3
    })
    .expect(200);
  
  // Step 2: Get cart contents
  const cartResponse = await request(app)
    .get('/api/cart')
    .set('Authorization', `Bearer ${userToken}`)
    .expect(200);
  
  expect(cartResponse.body.items).toHaveLength(1);
  
  // Step 3: Simulate user logging out and back in (new token)
  const user = await db.findUserByToken(userToken);
  const newToken = user.generateToken();
  
  // Step 4: Verify cart is persisted across sessions
  const persistedCartResponse = await request(app)
    .get('/api/cart')
    .set('Authorization', `Bearer ${newToken}`)
    .expect(200);
  
  expect(persistedCartResponse.body.items).toHaveLength(1);
  expect(persistedCartResponse.body.items[0].productId).toBe(productIds[0]);
  expect(persistedCartResponse.body.items[0].quantity).toBe(3);
  
  // Step 5: Update cart quantity
  await request(app)
    .put('/api/cart/items')
    .set('Authorization', `Bearer ${newToken}`)
    .send({
      productId: productIds[0],
      quantity: 5
    })
    .expect(200);
  
  // Step 6: Verify quantity was updated
  const updatedCartResponse = await request(app)
    .get('/api/cart')
    .set('Authorization', `Bearer ${newToken}`)
    .expect(200);
  
  expect(updatedCartResponse.body.items[0].quantity).toBe(5);
  
  // Step 7: Remove item from cart
  await request(app)
    .delete(`/api/cart/items/${productIds[0]}`)
    .set('Authorization', `Bearer ${newToken}`)
    .expect(200);
  
  // Step 8: Verify cart is empty
  const emptyCartResponse = await request(app)
    .get('/api/cart')
    .set('Authorization', `Bearer ${newToken}`)
    .expect(200);
  
  expect(emptyCartResponse.body.items).toHaveLength(0);
});
});

API Testing CI/CD Integration

GitHub Actions Configuration for API Tests

// .github/workflows/api-tests.yml
name: API Tests

on:
push:
  branches: [ main, develop ]
  paths:
    - 'src/**'
    - 'test/**'
    - 'package.json'
    - 'package-lock.json'
pull_request:
  branches: [ main, develop ]
  paths:
    - 'src/**'
    - 'test/**'
    - 'package.json'
    - 'package-lock.json'

jobs:
test:
  runs-on: ubuntu-latest

  services:
    mongodb:
      image: mongo:4.4
      ports:
        - 27017:27017

  strategy:
    matrix:
      node-version: [14.x, 16.x]

  steps:
    - uses: actions/checkout@v2

    - name: Use Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v2
      with:
        node-version: ${{ matrix.node-version }}
        cache: 'npm'

    - name: Install dependencies
      run: npm ci

    - name: Run linting
      run: npm run lint

    - name: Run unit tests
      run: npm run test:unit

    - name: Run API integration tests
      run: npm run test:api
      env:
        TEST_DB_URL: mongodb://localhost:27017/test-db
        JWT_SECRET: test-secret-key

    - name: Generate test report
      if: always()
      run: npm run test:report

    - name: Upload test results
      if: always()
      uses: actions/upload-artifact@v2
      with:
        name: test-results
        path: |
          test-results/
          coverage/
          junit.xml

functional-tests:
  needs: test
  runs-on: ubuntu-latest
  if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop')

  services:
    mongodb:
      image: mongo:4.4
      ports:
        - 27017:27017

  steps:
    - uses: actions/checkout@v2

    - name: Use Node.js 16.x
      uses: actions/setup-node@v2
      with:
        node-version: 16.x
        cache: 'npm'

    - name: Install dependencies
      run: npm ci

    - name: Start API server
      run: npm run start:test &
      env:
        PORT: 3000
        NODE_ENV: test
        TEST_DB_URL: mongodb://localhost:27017/test-db
        JWT_SECRET: test-secret-key

    - name: Wait for server to start
      run: |
        echo "Waiting for server to start..."
        timeout 60 bash -c 'until curl -s http://localhost:3000/api/health; do sleep 1; done'
        echo "Server started!"

    - name: Run functional tests
      run: npm run test:functional
      env:
        TEST_DB_URL: mongodb://localhost:27017/test-db
        JWT_SECRET: test-secret-key

    - name: Generate functional test report
      if: always()
      run: npm run test:functional:report

    - name: Upload functional test results
      if: always()
      uses: actions/upload-artifact@v2
      with:
        name: functional-test-results
        path: functional-test-results/

performance-tests:
  needs: functional-tests
  runs-on: ubuntu-latest
  if: github.event_name == 'push' && github.ref == 'refs/heads/main'

  services:
    mongodb:
      image: mongo:4.4
      ports:
        - 27017:27017

  steps:
    - uses: actions/checkout@v2

    - name: Use Node.js 16.x
      uses: actions/setup-node@v2
      with:
        node-version: 16.x
        cache: 'npm'

    - name: Install dependencies
      run: npm ci

    - name: Start API server
      run: npm run start:prod &
      env:
        PORT: 3000
        NODE_ENV: production
        DB_URL: mongodb://localhost:27017/test-db
        JWT_SECRET: test-secret-key

    - name: Wait for server to start
      run: |
        echo "Waiting for server to start..."
        timeout 60 bash -c 'until curl -s http://localhost:3000/api/health; do sleep 1; done'
        echo "Server started!"

    - name: Install Artillery
      run: npm install -g artillery

    - name: Generate test data
      run: node ./scripts/generate-test-data.js

    - name: Run performance tests
      run: artillery run ./load-tests/artillery-config.yml -o load-test-results.json

    - name: Generate performance test report
      run: artillery report load-test-results.json -o load-test-report.html

    - name: Upload performance test results
      uses: actions/upload-artifact@v2
      with:
        name: performance-test-results
        path: |
          load-test-results.json
          load-test-report.html

Best Practices for API Testing

graph TD A[API Testing
Best Practices] --> B[Isolate Tests] A --> C[Test at Multiple Levels] A --> D[Use Realistic Data] A --> E[Verify Side Effects] A --> F[Test Error Cases] A --> G[Check Response Headers] A --> H[Test Relationships] style A fill:#f5f5f5,stroke:#333333 style B fill:#d1e7dd,stroke:#0f5132 style C fill:#d1e7dd,stroke:#0f5132 style D fill:#d1e7dd,stroke:#0f5132 style E fill:#d1e7dd,stroke:#0f5132 style F fill:#d1e7dd,stroke:#0f5132 style G fill:#d1e7dd,stroke:#0f5132 style H fill:#d1e7dd,stroke:#0f5132

Isolate Tests

Test at Multiple Levels

Use Realistic Data

Verify Side Effects

Test Error Cases

Check Response Headers

Test Relationships

Practical Test Organization

A well-organized test suite might look like:

test/
├── unit/
│   ├── controllers/
│   ├── services/
│   └── utils/
├── integration/
│   ├── api/
│   └── db/
├── functional/
│   ├── workflows/
│   └── scenarios/
├── schemas/
├── fixtures/
└── utils/

Practice Activities

Activity 1: Test a CRUD API

Create a simple API for managing blog posts with the following endpoints:

  • GET /api/posts - Get all posts
  • GET /api/posts/:id - Get a post by ID
  • POST /api/posts - Create a new post
  • PUT /api/posts/:id - Update a post
  • DELETE /api/posts/:id - Delete a post

Then write comprehensive tests for all endpoints, including:

  • Happy path tests for each endpoint
  • Error case tests (validation errors, not found, etc.)
  • Schema validation tests
  • Authentication tests (if implemented)

Activity 2: Test a Multi-step API Workflow

Create and test a multi-step API workflow such as:

  • User registration and profile management
  • Shopping cart and checkout process
  • Task creation and assignment workflow

Your tests should:

  • Follow users through the entire workflow
  • Verify state changes at each step
  • Test error scenarios and recovery
  • Test concurrent operations (if applicable)

Activity 3: API Performance Testing

Set up performance tests for an existing API:

  • Use Artillery to create a load test configuration
  • Design realistic user scenarios and flows
  • Run tests with different concurrency levels
  • Analyze results and identify bottlenecks
  • Implement at least one optimization based on test results

Compare performance before and after your optimization.

Further Reading