> ## Documentation Index
> Fetch the complete documentation index at: https://docs.loremstock.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Usability Testing for Backend APIs with Postman and Supertest

> Test API usability with Postman collections, Newman CLI, Supertest integration tests against a real MySQL test database, Joi validation, and consistent error responses.

Usability testing for a backend API means verifying that the API behaves correctly and predictably from the consumer's perspective. This includes testing that error messages are meaningful, input validation rejects bad data with clear feedback, and behaviors like pagination work as documented. This page covers Postman, Newman, Supertest with a real MySQL test database, and Joi validation.

## What Is API Usability Testing

For a backend API, usability testing answers:

* Do endpoints return correct HTTP status codes in all cases?
* Are error messages informative and consistent?
* Is request validation comprehensive and helpful?
* Does pagination, filtering, and sorting work as expected?
* Do all documented endpoints behave as documented?

<Note>
  API usability testing sits between unit testing (mocked DB) and full E2E testing. It tests complete HTTP request-response cycles against the running application with a real test database.
</Note>

## Integration Testing vs Unit Testing

| Aspect     | Unit Testing                         | Integration Testing            |
| ---------- | ------------------------------------ | ------------------------------ |
| Database   | Mocked (pool.execute is a jest.fn()) | Real MySQL test database       |
| Speed      | Very fast (ms)                       | Slower (hundreds of ms)        |
| Confidence | Tests logic                          | Tests full stack including SQL |
| Tools      | Jest + jest.mock()                   | Supertest + real MySQL         |

## Postman: Manual API Testing

### Creating a Collection

<Steps>
  <Step title="Create collection">
    Click Collections in sidebar, then + to create "SWDBD401 API Tests".
  </Step>

  <Step title="Add folders per resource">
    Create folders: Auth, Users, Posts. Organize requests inside.
  </Step>

  <Step title="Set environment variables">
    Create an environment: baseUrl = [http://localhost:3000](http://localhost:3000), token = (empty initially).
  </Step>

  <Step title="Write test scripts">
    Add test scripts in the Tests tab of each request.
  </Step>
</Steps>

### Postman Test Scripts

```javascript theme={null}
// POST /auth/login — capture token for subsequent requests
pm.test("Login returns 200", function () {
  pm.response.to.have.status(200);
});

pm.test("Response has token", function () {
  const json = pm.response.json();
  pm.expect(json).to.have.property('token');
  pm.environment.set('token', json.token); // Save for other requests
});

// GET /users — check array response
pm.test("Returns array of users", function () {
  const json = pm.response.json();
  pm.expect(json.data).to.be.an('array');
  pm.expect(json.pagination.total).to.be.a('number');
});

// POST /users without email — check 400
pm.test("Returns 400 on missing email", function () {
  pm.response.to.have.status(400);
  const json = pm.response.json();
  pm.expect(json.message).to.be.a('string');
});
```

### Newman: CLI Runner

```bash theme={null}
npm install -g newman

# Export collection from Postman: Collection > ... > Export v2.1

newman run collection.json
newman run collection.json --environment environment.json

# HTML report
npm install -g newman-reporter-htmlextra
newman run collection.json --reporters htmlextra --reporter-htmlextra-export report.html
```

## MySQL Test Database Setup

Never run integration tests against your development or production database.

```bash theme={null}
# Create a test database in MySQL
mysql -u root -p -e "CREATE DATABASE swdbd_test;"
```

```env theme={null}
# .env.test
DB_HOST=localhost
DB_PORT=3306
DB_USER=root
DB_PASS=yourpassword
DB_NAME=swdbd_test
NODE_ENV=test
JWT_SECRET=test-secret-key-at-least-32-characters-long
PORT=3001
```

```json theme={null}
{
  "scripts": {
    "test": "cross-env NODE_ENV=test jest --forceExit"
  },
  "jest": {
    "testEnvironment": "node",
    "setupFiles": ["dotenv/config"],
    "testTimeout": 30000
  }
}
```

## Database Seeding and Teardown

```javascript theme={null}
// tests/helpers/db.js
const pool = require('../../config/db');

async function clearTables() {
  await pool.execute('SET FOREIGN_KEY_CHECKS = 0');
  await pool.execute('TRUNCATE TABLE audit_logs');
  await pool.execute('TRUNCATE TABLE users');
  await pool.execute('SET FOREIGN_KEY_CHECKS = 1');
}

async function seedUsers(users) {
  const bcrypt = require('bcrypt');
  for (const user of users) {
    const hashed = await bcrypt.hash(user.password, 10);
    await pool.execute(
      'INSERT INTO users (name, email, password, role) VALUES (?, ?, ?, ?)',
      [user.name, user.email, hashed, user.role || 'user']
    );
  }
}

module.exports = { clearTables, seedUsers };
```

## Supertest Integration Tests

```javascript theme={null}
// tests/users.integration.test.js
const request = require('supertest');
const app = require('../app');
const pool = require('../config/db');
const jwt = require('jsonwebtoken');
const { clearTables, seedUsers } = require('./helpers/db');

let adminToken;

beforeAll(async () => {
  adminToken = jwt.sign({ sub: 1, role: 'admin' }, process.env.JWT_SECRET, { expiresIn: '1h' });
});

beforeEach(async () => {
  await clearTables();
  await seedUsers([
    { name: 'Alice', email: 'alice@test.com', password: 'password123', role: 'admin' },
    { name: 'Bob', email: 'bob@test.com', password: 'password123', role: 'user' }
  ]);
});

afterAll(async () => {
  await pool.end(); // Close all pool connections
});

describe('POST /api/v1/users', () => {
  it('creates a user and returns 201', async () => {
    const res = await request(app)
      .post('/api/v1/users')
      .send({ name: 'Carol', email: 'carol@test.com', password: 'password123' });

    expect(res.status).toBe(201);
    expect(res.body.data.email).toBe('carol@test.com');
    expect(res.body.data.password).toBeUndefined();
  });

  it('returns 400 with message when email is missing', async () => {
    const res = await request(app)
      .post('/api/v1/users')
      .send({ name: 'Dan', password: 'password123' });

    expect(res.status).toBe(400);
    expect(res.body.message).toBeTruthy();
  });

  it('returns 409 on duplicate email', async () => {
    const res = await request(app)
      .post('/api/v1/users')
      .send({ name: 'Alice2', email: 'alice@test.com', password: 'password123' });

    expect(res.status).toBe(409);
  });
});

describe('GET /api/v1/users (paginated)', () => {
  it('returns paginated users', async () => {
    const res = await request(app)
      .get('/api/v1/users?page=1&limit=1')
      .set('Authorization', `Bearer ${adminToken}`);

    expect(res.status).toBe(200);
    expect(res.body.data).toHaveLength(1);
    expect(res.body.pagination.total).toBe(2);
    expect(res.body.pagination.pages).toBe(2);
  });
});
```

## Input Validation with Joi

```bash theme={null}
npm install joi
```

```javascript theme={null}
// validation/userValidation.js
const Joi = require('joi');

const createUserSchema = Joi.object({
  name: Joi.string().trim().min(2).max(100).required().messages({
    'string.min': 'Name must be at least 2 characters',
    'any.required': 'Name is required'
  }),
  email: Joi.string().email().lowercase().required().messages({
    'string.email': 'Please provide a valid email',
    'any.required': 'Email is required'
  }),
  password: Joi.string().min(8).required().messages({
    'string.min': 'Password must be at least 8 characters',
    'any.required': 'Password is required'
  }),
  role: Joi.string().valid('user', 'admin').default('user')
});

module.exports = { createUserSchema };
```

```javascript theme={null}
// middleware/validate.js
function validate(schema) {
  return (req, res, next) => {
    const { error, value } = schema.validate(req.body, { abortEarly: false });
    if (error) {
      const errors = error.details.map(d => ({ field: d.path.join('.'), message: d.message }));
      return res.status(422).json({ message: 'Validation failed', errors });
    }
    req.body = value;
    next();
  };
}
module.exports = validate;
```

## Testing Error Response Quality

```javascript theme={null}
describe('Error response quality', () => {
  it('validation error includes field names', async () => {
    const res = await request(app)
      .post('/api/v1/users')
      .send({ name: 'x' });

    expect(res.status).toBe(422);
    expect(res.body.errors).toBeInstanceOf(Array);
    expect(res.body.errors[0]).toHaveProperty('field');
    expect(res.body.errors[0]).toHaveProperty('message');
  });

  it('returns 401 on protected route without token', async () => {
    const res = await request(app).get('/api/v1/users');
    expect(res.status).toBe(401);
    expect(res.body.message).toBeTruthy();
  });
});
```

## Key Terms

| Term                         | Definition                                                                  |
| ---------------------------- | --------------------------------------------------------------------------- |
| **Integration test**         | Test that verifies multiple components (controller + MySQL) work together   |
| **API contract**             | Documented agreement about endpoint behavior: inputs, outputs, status codes |
| **Postman collection**       | Saved set of API requests organized in folders                              |
| **Newman**                   | CLI tool to run Postman collections in CI/CD pipelines                      |
| **Joi**                      | Schema validation library for JavaScript objects                            |
| **Test database**            | Separate MySQL database used only during tests                              |
| **422 Unprocessable Entity** | Status for syntactically valid requests that fail business validation rules |

## Common Mistakes

<Accordion title="Running integration tests against the development database">
  Tests truncate tables. Always use a separate DB\_NAME\_TEST database via .env.test and cross-env NODE\_ENV=test.
</Accordion>

<Accordion title="Not closing the MySQL pool after tests">
  If pool.end() is not called in afterAll, Jest hangs after tests complete because the pool keeps Node.js alive.
</Accordion>

<Accordion title="Only testing happy paths">
  Usability testing must cover error paths: missing fields, duplicate emails, unauthorized access, not found IDs. Users always hit these paths.
</Accordion>

<Accordion title="Returning 500 for validation failures">
  Client input mistakes should return 400 or 422, not 500. A 500 hides the real cause and forces clients to guess what went wrong.
</Accordion>
