Skip to content

Latest commit

 

History

History
660 lines (563 loc) · 16.2 KB

File metadata and controls

660 lines (563 loc) · 16.2 KB

API Testing Guide

Comprehensive API testing is essential for reliable frontend applications. This guide covers tools like Postman, Hoppscotch, Insomnia, and automated testing strategies for API endpoints.

🚀 Getting Started

API Testing Tools

# CLI tools
npm install -g newman  # Postman CLI
npm install -g hoppscotch-cli

# Testing libraries
npm install --save-dev axios supertest jest
npm install --save-dev @testing-library/react @testing-library/jest-dom

Quick Setup

// test-setup.js
import '@testing-library/jest-dom';

// Mock fetch globally
global.fetch = jest.fn();

// Setup test environment
beforeEach(() => {
  fetch.mockClear();
});

🛠️ Postman Testing

Collection Structure

{
  "info": {
    "name": "User API Tests",
    "description": "Comprehensive tests for user management API"
  },
  "item": [
    {
      "name": "Authentication",
      "item": [
        {
          "name": "Login",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"email\": \"test@example.com\",\n  \"password\": \"password123\"\n}"
            },
            "url": {
              "raw": "{{baseUrl}}/auth/login",
              "host": ["{{baseUrl}}"],
              "path": ["auth", "login"]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "pm.test(\"Status code is 200\", function () {",
                  "    pm.response.to.have.status(200);",
                  "});",
                  "",
                  "pm.test(\"Response has token\", function () {",
                  "    const jsonData = pm.response.json();",
                  "    pm.expect(jsonData).to.have.property('token');",
                  "    pm.environment.set('authToken', jsonData.token);",
                  "});",
                  "",
                  "pm.test(\"Response time is less than 1000ms\", function () {",
                  "    pm.expect(pm.response.responseTime).to.be.below(1000);",
                  "});"
                ]
              }
            }
          ]
        }
      ]
    }
  ]
}

Environment Variables

{
  "id": "api-env",
  "name": "API Environment",
  "values": [
    {
      "key": "baseUrl",
      "value": "https://api.example.com",
      "enabled": true
    },
    {
      "key": "authToken",
      "value": "",
      "enabled": true
    },
    {
      "key": "userId",
      "value": "",
      "enabled": true
    }
  ]
}

Pre-request Scripts

// Generate dynamic data
const timestamp = Date.now();
pm.environment.set('timestamp', timestamp);
pm.environment.set('randomEmail', `test${timestamp}@example.com`);

// Set authentication headers
const token = pm.environment.get('authToken');
if (token) {
  pm.request.headers.add({
    key: 'Authorization',
    value: `Bearer ${token}`
  });
}

// Add request ID for tracking
pm.request.headers.add({
  key: 'X-Request-ID',
  value: pm.variables.replaceIn('{{$randomUUID}}')
});

Test Scripts

// Response validation
pm.test("Response is JSON", function () {
    pm.response.to.be.json;
});

pm.test("Response has required fields", function () {
    const jsonData = pm.response.json();
    pm.expect(jsonData).to.have.property('id');
    pm.expect(jsonData).to.have.property('name');
    pm.expect(jsonData).to.have.property('email');
});

pm.test("Response time is acceptable", function () {
    pm.expect(pm.response.responseTime).to.be.below(2000);
});

pm.test("Response headers are correct", function () {
    pm.expect(pm.response.headers.get('Content-Type')).to.include('application/json');
});

// Data type validation
pm.test("ID is a number", function () {
    const jsonData = pm.response.json();
    pm.expect(jsonData.id).to.be.a('number');
});

pm.test("Email format is valid", function () {
    const jsonData = pm.response.json();
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    pm.expect(jsonData.email).to.match(emailRegex);
});

// Status code validation
pm.test("Success status code", function () {
    pm.expect(pm.response.code).to.be.oneOf([200, 201]);
});

🎯 Hoppscotch Testing

Collection Setup

{
  "v": 1,
  "name": "User API Collection",
  "folders": [
    {
      "name": "Authentication",
      "requests": [
        {
          "name": "Login",
          "method": "POST",
          "endpoint": "{{baseUrl}}/auth/login",
          "headers": [
            {
              "key": "Content-Type",
              "value": "application/json"
            }
          ],
          "body": {
            "contentType": "application/json",
            "body": "{\n  \"email\": \"{{testEmail}}\",\n  \"password\": \"{{testPassword}}\"\n}"
          },
          "tests": "tests.login"
        }
      ]
    }
  ]
}

Test Scripts

// tests/login.js
pw.test("Login successful", function () {
    pw.expect(pw.response.status).toBe(200);
});

pw.test("Response has token", function () {
    const response = pw.response.json();
    pw.expect(response).toHaveProperty("token");
    pw.environment.set("authToken", response.token);
});

pw.test("Response time under 1 second", function () {
    pw.expect(pw.response.responseTime).toBeLessThan(1000);
});

🔧 Automated Testing

Jest API Tests

// tests/api/user.test.js
const axios = require('axios');

const API_BASE = process.env.API_BASE || 'http://localhost:3000/api';

describe('User API', () => {
  let authToken;
  let userId;

  beforeAll(async () => {
    // Setup test data
    const loginResponse = await axios.post(`${API_BASE}/auth/login`, {
      email: 'test@example.com',
      password: 'password123'
    });
    authToken = loginResponse.data.token;
  });

  describe('GET /users', () => {
    it('should return list of users', async () => {
      const response = await axios.get(`${API_BASE}/users`, {
        headers: { Authorization: `Bearer ${authToken}` }
      });

      expect(response.status).toBe(200);
      expect(Array.isArray(response.data)).toBe(true);
      expect(response.data.length).toBeGreaterThan(0);
    });

    it('should support pagination', async () => {
      const response = await axios.get(`${API_BASE}/users?page=1&limit=10`, {
        headers: { Authorization: `Bearer ${authToken}` }
      });

      expect(response.status).toBe(200);
      expect(response.data.length).toBeLessThanOrEqual(10);
    });

    it('should filter users by query', async () => {
      const response = await axios.get(`${API_BASE}/users?search=john`, {
        headers: { Authorization: `Bearer ${authToken}` }
      });

      expect(response.status).toBe(200);
      expect(response.data.every(user => 
        user.name.toLowerCase().includes('john')
      )).toBe(true);
    });
  });

  describe('POST /users', () => {
    it('should create a new user', async () => {
      const userData = {
        name: 'Test User',
        email: `test${Date.now()}@example.com`,
        password: 'password123'
      };

      const response = await axios.post(`${API_BASE}/users`, userData, {
        headers: { Authorization: `Bearer ${authToken}` }
      });

      expect(response.status).toBe(201);
      expect(response.data).toHaveProperty('id');
      expect(response.data.name).toBe(userData.name);
      expect(response.data.email).toBe(userData.email);
      
      userId = response.data.id;
    });

    it('should validate required fields', async () => {
      const response = await axios.post(`${API_BASE}/users`, {}, {
        headers: { Authorization: `Bearer ${authToken}` },
        validateStatus: () => true
      });

      expect(response.status).toBe(400);
      expect(response.data).toHaveProperty('errors');
    });
  });

  describe('PUT /users/:id', () => {
    it('should update user', async () => {
      const updateData = { name: 'Updated Name' };
      
      const response = await axios.put(`${API_BASE}/users/${userId}`, updateData, {
        headers: { Authorization: `Bearer ${authToken}` }
      });

      expect(response.status).toBe(200);
      expect(response.data.name).toBe(updateData.name);
    });

    it('should return 404 for non-existent user', async () => {
      const response = await axios.put(`${API_BASE}/users/99999`, { name: 'Test' }, {
        headers: { Authorization: `Bearer ${authToken}` },
        validateStatus: () => true
      });

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

  describe('DELETE /users/:id', () => {
    it('should delete user', async () => {
      const response = await axios.delete(`${API_BASE}/users/${userId}`, {
        headers: { Authorization: `Bearer ${authToken}` }
      });

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

Supertest Integration

// tests/api/supertest.test.js
const request = require('supertest');
const app = require('../../app');

describe('API Endpoints', () => {
  let authToken;

  beforeAll(async () => {
    const response = await request(app)
      .post('/api/auth/login')
      .send({
        email: 'test@example.com',
        password: 'password123'
      });
    
    authToken = response.body.token;
  });

  describe('User Management', () => {
    it('should create and retrieve user', async () => {
      const userData = {
        name: 'Test User',
        email: `test${Date.now()}@example.com`,
        password: 'password123'
      };

      // Create user
      const createResponse = await request(app)
        .post('/api/users')
        .set('Authorization', `Bearer ${authToken}`)
        .send(userData)
        .expect(201);

      const userId = createResponse.body.id;

      // Retrieve user
      const getResponse = await request(app)
        .get(`/api/users/${userId}`)
        .set('Authorization', `Bearer ${authToken}`)
        .expect(200);

      expect(getResponse.body.name).toBe(userData.name);
      expect(getResponse.body.email).toBe(userData.email);
    });

    it('should handle validation errors', async () => {
      await request(app)
        .post('/api/users')
        .set('Authorization', `Bearer ${authToken}`)
        .send({ name: 'Test' }) // Missing required fields
        .expect(400)
        .expect((res) => {
          expect(res.body.errors).toBeDefined();
          expect(res.body.errors.email).toBeDefined();
        });
    });
  });
});

🎯 Performance Testing

Load Testing with Artillery

# artillery-config.yml
config:
  target: 'https://api.example.com'
  phases:
    - duration: 60
      arrivalRate: 10
    - duration: 120
      arrivalRate: 20
    - duration: 60
      arrivalRate: 10

scenarios:
  - name: "User API Load Test"
    weight: 100
    flow:
      - post:
          url: "/auth/login"
          json:
            email: "test@example.com"
            password: "password123"
          capture:
            - json: "$.token"
              as: "authToken"
      - get:
          url: "/users"
          headers:
            Authorization: "Bearer {{ authToken }}"
      - post:
          url: "/users"
          headers:
            Authorization: "Bearer {{ authToken }}"
          json:
            name: "Load Test User"
            email: "loadtest{{ $randomInt(1, 10000) }}@example.com"
            password: "password123"

Performance Test Scripts

// tests/performance/api.test.js
const axios = require('axios');

describe('API Performance Tests', () => {
  const API_BASE = process.env.API_BASE || 'http://localhost:3000/api';
  const PERFORMANCE_THRESHOLD = 1000; // 1 second

  it('should respond within performance threshold', async () => {
    const startTime = Date.now();
    
    const response = await axios.get(`${API_BASE}/users`);
    
    const responseTime = Date.now() - startTime;
    
    expect(response.status).toBe(200);
    expect(responseTime).toBeLessThan(PERFORMANCE_THRESHOLD);
  });

  it('should handle concurrent requests', async () => {
    const concurrentRequests = 10;
    const requests = Array(concurrentRequests).fill().map(() => 
      axios.get(`${API_BASE}/users`)
    );

    const startTime = Date.now();
    const responses = await Promise.all(requests);
    const totalTime = Date.now() - startTime;

    expect(responses.every(res => res.status === 200)).toBe(true);
    expect(totalTime).toBeLessThan(PERFORMANCE_THRESHOLD * 2);
  });
});

🔧 Mock API Testing

MSW (Mock Service Worker)

// tests/mocks/handlers.js
import { rest } from 'msw';

export const handlers = [
  rest.get('/api/users', (req, res, ctx) => {
    return res(
      ctx.status(200),
      ctx.json([
        { id: 1, name: 'John Doe', email: 'john@example.com' },
        { id: 2, name: 'Jane Smith', email: 'jane@example.com' }
      ])
    );
  }),

  rest.post('/api/users', (req, res, ctx) => {
    const { name, email } = req.body;
    return res(
      ctx.status(201),
      ctx.json({
        id: Date.now(),
        name,
        email,
        createdAt: new Date().toISOString()
      })
    );
  }),

  rest.put('/api/users/:id', (req, res, ctx) => {
    const { id } = req.params;
    const { name, email } = req.body;
    
    return res(
      ctx.status(200),
      ctx.json({
        id: parseInt(id),
        name,
        email,
        updatedAt: new Date().toISOString()
      })
    );
  }),

  rest.delete('/api/users/:id', (req, res, ctx) => {
    return res(ctx.status(204));
  })
];

MSW Setup

// tests/setup.js
import { setupServer } from 'msw/node';
import { handlers } from './mocks/handlers';

export const server = setupServer(...handlers);

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

📊 API Testing Best Practices

Test Data Management

// tests/fixtures/users.js
export const testUsers = {
  validUser: {
    name: 'Test User',
    email: 'test@example.com',
    password: 'password123'
  },
  invalidUser: {
    name: 'Test',
    email: 'invalid-email',
    password: '123'
  },
  adminUser: {
    name: 'Admin User',
    email: 'admin@example.com',
    password: 'admin123',
    role: 'admin'
  }
};

// tests/helpers/testData.js
export function generateTestUser(overrides = {}) {
  return {
    name: 'Test User',
    email: `test${Date.now()}@example.com`,
    password: 'password123',
    ...overrides
  };
}

Error Testing

// tests/api/error-handling.test.js
describe('Error Handling', () => {
  it('should handle network errors gracefully', async () => {
    // Mock network error
    axios.interceptors.request.use(() => {
      throw new Error('Network Error');
    });

    try {
      await axios.get(`${API_BASE}/users`);
    } catch (error) {
      expect(error.message).toBe('Network Error');
    }
  });

  it('should handle server errors', async () => {
    const response = await axios.get(`${API_BASE}/users`, {
      validateStatus: () => true
    });

    if (response.status >= 500) {
      expect(response.data).toHaveProperty('error');
    }
  });
});

Security Testing

// tests/api/security.test.js
describe('Security Tests', () => {
  it('should require authentication', async () => {
    const response = await axios.get(`${API_BASE}/users`, {
      validateStatus: () => true
    });

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

  it('should validate JWT tokens', async () => {
    const response = await axios.get(`${API_BASE}/users`, {
      headers: { Authorization: 'Bearer invalid-token' },
      validateStatus: () => true
    });

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

  it('should prevent SQL injection', async () => {
    const maliciousQuery = "'; DROP TABLE users; --";
    
    const response = await axios.get(`${API_BASE}/users?search=${maliciousQuery}`, {
      validateStatus: () => true
    });

    // Should not cause server error
    expect(response.status).not.toBe(500);
  });
});

📚 Additional Resources