backend-testing

Comprehensive backend testing for REST APIs, databases, authentication, and business logic across Jest, Pytest, and Mocha. Covers unit tests for isolated functions, integration tests for API endpoints, and authentication/authorization flows with detailed examples for Express.js, FastAPI, and other frameworks Includes test environment setup, mocking strategies for external dependencies, and database isolation patterns using in-memory or separate test databases Provides step-by-step guidance on AAA pattern (Arrange-Act-Assert), test fixtures, and role-based access control testing with JWT tokens Enforces test isolation, clear naming conventions, and coverage thresholds (default 80%) to prevent shared state issues and ensure reliable test execution

INSTALLATION
npx skills add https://github.com/supercent-io/skills-template --skill backend-testing
Run in your project or agent environment. Adjust flags if your CLI version differs.

SKILL.md

Backend Testing

When to use this skill

Specific situations that should trigger this skill:

  • New feature development: Write tests first using TDD (Test-Driven Development)
  • Adding API endpoints: Test success and failure cases for REST APIs
  • Bug fixes: Add tests to prevent regressions
  • Before refactoring: Write tests that guarantee existing behavior
  • CI/CD setup: Build automated test pipelines

Input Format

Format and required/optional information to collect from the user:

Required information

  • Framework: Express, Django, FastAPI, Spring Boot, etc.
  • Test tool: Jest, Pytest, Mocha/Chai, JUnit, etc.
  • Test target: API endpoints, business logic, DB operations, etc.

Optional information

  • Database: PostgreSQL, MySQL, MongoDB (default: in-memory DB)
  • Mocking library: jest.mock, sinon, unittest.mock (default: framework built-in)
  • Coverage target: 80%, 90%, etc. (default: 80%)
  • E2E tool: Supertest, TestClient, RestAssured (optional)

Input example

Test the user authentication endpoints for an Express.js API:

- Framework: Express + TypeScript

- Test tool: Jest + Supertest

- Target: POST /auth/register, POST /auth/login

- DB: PostgreSQL (in-memory for tests)

- Coverage: 90% or above

Instructions

Step-by-step task order to follow precisely.

Step 1: Set up the test environment

Install and configure the test framework and tools.

Tasks:

  • Install test libraries
  • Configure test database (in-memory or separate DB)
  • Separate environment variables (.env.test)
  • Configure jest.config.js or pytest.ini

Example (Node.js + Jest + Supertest):

npm install --save-dev jest ts-jest @types/jest supertest @types/supertest

jest.config.js:

module.exports = {

  preset: 'ts-jest',

  testEnvironment: 'node',

  roots: ['<rootDir>/src'],

  testMatch: ['**/__tests__/**/*.test.ts'],

  collectCoverageFrom: [

    'src/**/*.ts',

    '!src/**/*.d.ts',

    '!src/__tests__/**'

  ],

  coverageThreshold: {

    global: {

      branches: 80,

      functions: 80,

      lines: 80,

      statements: 80

    }

  },

  setupFilesAfterEnv: ['<rootDir>/src/__tests__/setup.ts']

};

setup.ts (global test configuration):

import { db } from '../database';

// Reset DB before each test

beforeEach(async () => {

  await db.migrate.latest();

  await db.seed.run();

});

// Clean up after each test

afterEach(async () => {

  await db.migrate.rollback();

});

// Close connection after all tests complete

afterAll(async () => {

  await db.destroy();

});

Step 2: Write Unit Tests (business logic)

Write unit tests for individual functions and classes.

Tasks:

  • Test pure functions (no dependencies)
  • Isolate dependencies via mocking
  • Test edge cases (boundary values, exceptions)
  • AAA pattern (Arrange-Act-Assert)

Decision criteria:

  • No external dependencies (DB, API) -> pure Unit Test
  • External dependencies present -> use Mock/Stub
  • Complex logic -> test various input cases

Example (password validation function):

// src/utils/password.ts

export function validatePassword(password: string): { valid: boolean; errors: string[] } {

  const errors: string[] = [];

  if (password.length < 8) {

    errors.push('Password must be at least 8 characters');

  }

  if (!/[A-Z]/.test(password)) {

    errors.push('Password must contain uppercase letter');

  }

  if (!/[a-z]/.test(password)) {

    errors.push('Password must contain lowercase letter');

  }

  if (!/\d/.test(password)) {

    errors.push('Password must contain number');

  }

  if (!/[!@#$%^&#x26;*]/.test(password)) {

    errors.push('Password must contain special character');

  }

  return { valid: errors.length === 0, errors };

}

// src/__tests__/utils/password.test.ts

import { validatePassword } from '../../utils/password';

describe('validatePassword', () => {

  it('should accept valid password', () => {

    const result = validatePassword('Password123!');

    expect(result.valid).toBe(true);

    expect(result.errors).toHaveLength(0);

  });

  it('should reject password shorter than 8 characters', () => {

    const result = validatePassword('Pass1!');

    expect(result.valid).toBe(false);

    expect(result.errors).toContain('Password must be at least 8 characters');

  });

  it('should reject password without uppercase', () => {

    const result = validatePassword('password123!');

    expect(result.valid).toBe(false);

    expect(result.errors).toContain('Password must contain uppercase letter');

  });

  it('should reject password without lowercase', () => {

    const result = validatePassword('PASSWORD123!');

    expect(result.valid).toBe(false);

    expect(result.errors).toContain('Password must contain lowercase letter');

  });

  it('should reject password without number', () => {

    const result = validatePassword('Password!');

    expect(result.valid).toBe(false);

    expect(result.errors).toContain('Password must contain number');

  });

  it('should reject password without special character', () => {

    const result = validatePassword('Password123');

    expect(result.valid).toBe(false);

    expect(result.errors).toContain('Password must contain special character');

  });

  it('should return multiple errors for invalid password', () => {

    const result = validatePassword('pass');

    expect(result.valid).toBe(false);

    expect(result.errors.length).toBeGreaterThan(1);

  });

});

Step 3: Integration Test (API endpoints)

Write integration tests for API endpoints.

Tasks:

  • Test HTTP requests/responses
  • Success cases (200, 201)
  • Failure cases (400, 401, 404, 500)
  • Authentication/authorization tests
  • Input validation tests

Checklist:

  • Verify status code
  • Validate response body structure
  • Confirm database state changes
  • Validate error messages

Example (Express.js + Supertest):

// src/__tests__/api/auth.test.ts

import request from 'supertest';

import app from '../../app';

import { db } from '../../database';

describe('POST /auth/register', () => {

  it('should register new user successfully', async () => {

    const response = await request(app)

      .post('/api/auth/register')

      .send({

        email: 'test@example.com',

        username: 'testuser',

        password: 'Password123!'

      });

    expect(response.status).toBe(201);

    expect(response.body).toHaveProperty('user');

    expect(response.body).toHaveProperty('accessToken');

    expect(response.body.user.email).toBe('test@example.com');

    // Verify the record was actually saved to DB

    const user = await db.user.findUnique({ where: { email: 'test@example.com' } });

    expect(user).toBeTruthy();

    expect(user.username).toBe('testuser');

  });

  it('should reject duplicate email', async () => {

    // Create first user

    await request(app)

      .post('/api/auth/register')

      .send({

        email: 'test@example.com',

        username: 'user1',

        password: 'Password123!'

      });

    // Second attempt with same email

    const response = await request(app)

      .post('/api/auth/register')

      .send({

        email: 'test@example.com',

        username: 'user2',

        password: 'Password123!'

      });

    expect(response.status).toBe(409);

    expect(response.body.error).toContain('already exists');

  });

  it('should reject weak password', async () => {

    const response = await request(app)

      .post('/api/auth/register')

      .send({

        email: 'test@example.com',

        username: 'testuser',

        password: 'weak'

      });

    expect(response.status).toBe(400);

    expect(response.body.error).toBeDefined();

  });

  it('should reject missing fields', async () => {

    const response = await request(app)

      .post('/api/auth/register')

      .send({

        email: 'test@example.com'

        // username, password omitted

      });

    expect(response.status).toBe(400);

  });

});

describe('POST /auth/login', () => {

  beforeEach(async () => {

    // Create test user

    await request(app)

      .post('/api/auth/register')

      .send({

        email: 'test@example.com',

        username: 'testuser',

        password: 'Password123!'

      });

  });

  it('should login with valid credentials', async () => {

    const response = await request(app)

      .post('/api/auth/login')

      .send({

        email: 'test@example.com',

        password: 'Password123!'

      });

    expect(response.status).toBe(200);

    expect(response.body).toHaveProperty('accessToken');

    expect(response.body).toHaveProperty('refreshToken');

    expect(response.body.user.email).toBe('test@example.com');

  });

  it('should reject invalid password', async () => {

    const response = await request(app)

      .post('/api/auth/login')

      .send({

        email: 'test@example.com',

        password: 'WrongPassword123!'

      });

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

    expect(response.body.error).toContain('Invalid credentials');

  });

  it('should reject non-existent user', async () => {

    const response = await request(app)

      .post('/api/auth/login')

      .send({

        email: 'nonexistent@example.com',

        password: 'Password123!'

      });

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

  });

});

Step 4: Authentication/Authorization Tests

Test JWT tokens and role-based access control.

Tasks:

  • Confirm 401 when accessing without a token
  • Confirm successful access with a valid token
  • Test expired token handling
  • Role-based permission tests

Example:

describe('Protected Routes', () => {

  let accessToken: string;

  let adminToken: string;

  beforeEach(async () => {

    // Regular user token

    const userResponse = await request(app)

      .post('/api/auth/register')

      .send({

        email: 'user@example.com',

        username: 'user',

        password: 'Password123!'

      });

    accessToken = userResponse.body.accessToken;

    // Admin token

    const adminResponse = await request(app)

      .post('/api/auth/register')

      .send({

        email: 'admin@example.com',

        username: 'admin',

        password: 'Password123!'

      });

    // Update role to 'admin' in DB

    await db.user.update({

      where: { email: 'admin@example.com' },

      data: { role: 'admin' }

    });

    // Log in again to get a new token

    const loginResponse = await request(app)

      .post('/api/auth/login')

      .send({

        email: 'admin@example.com',

        password: 'Password123!'

      });

    adminToken = loginResponse.body.accessToken;

  });

  describe('GET /api/auth/me', () => {

    it('should return current user with valid token', async () => {

      const response = await request(app)

        .get('/api/auth/me')

        .set('Authorization', `Bearer ${accessToken}`);

      expect(response.status).toBe(200);

      expect(response.body.user.email).toBe('user@example.com');

    });

    it('should reject request without token', async () => {

      const response = await request(app)

        .get('/api/auth/me');

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

    });

    it('should reject request with invalid token', async () => {

      const response = await request(app)

        .get('/api/auth/me')

        .set('Authorization', 'Bearer invalid-token');

      expect(response.status).toBe(403);

    });

  });

  describe('DELETE /api/users/:id (Admin only)', () => {

    it('should allow admin to delete user', async () => {

      const targetUser = await db.user.findUnique({ where: { email: 'user@example.com' } });

      const response = await request(app)

        .delete(`/api/users/${targetUser.id}`)

        .set('Authorization', `Bearer ${adminToken}`);

      expect(response.status).toBe(200);

    });

    it('should forbid non-admin from deleting user', async () => {

      const targetUser = await db.user.findUnique({ where: { email: 'user@example.com' } });

      const response = await request(app)

        .delete(`/api/users/${targetUser.id}`)

        .set('Authorization', `Bearer ${accessToken}`);

      expect(response.status).toBe(403);

    });

  });

});

Step 5: Mocking and Test Isolation

Mock external dependencies to isolate tests.

Tasks:

  • Mock external APIs
  • Mock email sending
  • Mock file system
  • Mock time-related functions

Example (mocking an external API):

// src/services/emailService.ts

export async function sendVerificationEmail(email: string, token: string): Promise<void> {

  const response = await fetch('https://api.sendgrid.com/v3/mail/send', {

    method: 'POST',

    headers: { 'Authorization': `Bearer ${process.env.SENDGRID_API_KEY}` },

    body: JSON.stringify({

      to: email,

      subject: 'Verify your email',

      html: `<a href="https://example.com/verify?token=${token}">Verify</a>`

    })

  });

  if (!response.ok) {

    throw new Error('Failed to send email');

  }

}

// src/__tests__/services/emailService.test.ts

import { sendVerificationEmail } from '../../services/emailService';

// Mock fetch

global.fetch = jest.fn();

describe('sendVerificationEmail', () => {

  beforeEach(() => {

    (fetch as jest.Mock).mockClear();

  });

  it('should send email successfully', async () => {

    (fetch as jest.Mock).mockResolvedValueOnce({

      ok: true,

      status: 200

    });

    await expect(sendVerificationEmail('test@example.com', 'token123'))

      .resolves

      .toBeUndefined();

    expect(fetch).toHaveBeenCalledWith(

      'https://api.sendgrid.com/v3/mail/send',

      expect.objectContaining({

        method: 'POST'

      })

    );

  });

  it('should throw error if email sending fails', async () => {

    (fetch as jest.Mock).mockResolvedValueOnce({

      ok: false,

      status: 500

    });

    await expect(sendVerificationEmail('test@example.com', 'token123'))

      .rejects

      .toThrow('Failed to send email');

  });

});

Output format

Defines the exact format that outputs must follow.

Basic structure

project/

├── src/

│   ├── __tests__/

│   │   ├── setup.ts                 # Global test configuration

│   │   ├── utils/

│   │   │   └── password.test.ts     # Unit tests

│   │   ├── services/

│   │   │   └── emailService.test.ts

│   │   └── api/

│   │       ├── auth.test.ts         # Integration tests

│   │       └── users.test.ts

│   └── ...

├── jest.config.js

└── package.json

Test run scripts (package.json)

{

  "scripts": {

    "test": "jest",

    "test:watch": "jest --watch",

    "test:coverage": "jest --coverage",

    "test:ci": "jest --ci --coverage --maxWorkers=2"

  }

}

Coverage report

$ npm run test:coverage

--------------------------|---------|----------|---------|---------|

File                      | % Stmts | % Branch | % Funcs | % Lines |

--------------------------|---------|----------|---------|---------|

All files                 |   92.5  |   88.3   |   95.2  |   92.8  |

 auth/                    |   95.0  |   90.0   |  100.0  |   95.0  |

  middleware.ts           |   95.0  |   90.0   |  100.0  |   95.0  |

  routes.ts               |   95.0  |   90.0   |  100.0  |   95.0  |

 utils/                   |   90.0  |   85.0   |   90.0  |   90.0  |

  password.ts             |   90.0  |   85.0   |   90.0  |   90.0  |

--------------------------|---------|----------|---------|---------|

Constraints

Rules and prohibitions that must be strictly followed.

Required rules (MUST)

-

Test isolation: Each test must be runnable independently

  • Reset state with beforeEach/afterEach
  • Do not depend on test execution order

-

Clear test names: The name must convey what the test verifies

  • ✅ 'should reject duplicate email'
  • ❌ 'test1'

-

AAA pattern: Arrange (setup) - Act (execute) - Assert (verify) structure

  • Improves readability
  • Clarifies test intent

Prohibited (MUST NOT)

-

No production DB: Tests must use a separate or in-memory DB

  • Risk of losing real data
  • Cannot isolate tests

-

No real external API calls: Mock all external services

  • Removes network dependency
  • Speeds up tests
  • Reduces costs

-

No Sleep/Timeout abuse: Use fake timers for time-based tests

  • jest.useFakeTimers()
  • Prevents test slowdowns

Security rules

  • No hardcoded secrets: Never hardcode API keys or passwords in test code
  • Separate environment variables: Use .env.test file

Examples

Example 1: Python FastAPI tests (Pytest)

Situation: Testing a FastAPI REST API

User request:

Test the user API built with FastAPI using pytest.

Final result:

# tests/conftest.py

import pytest

from fastapi.testclient import TestClient

from sqlalchemy import create_engine

from sqlalchemy.orm import sessionmaker

from app.main import app

from app.database import Base, get_db

# In-memory SQLite for tests

SQLALCHEMY_DATABASE_URL = "sqlite:///./test.db"

engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False})

TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

@pytest.fixture(scope="function")

def db_session():

    Base.metadata.create_all(bind=engine)

    db = TestingSessionLocal()

    try:

        yield db

    finally:

        db.close()

        Base.metadata.drop_all(bind=engine)

@pytest.fixture(scope="function")

def client(db_session):

    def override_get_db():

        try:

            yield db_session

        finally:

            db_session.close()

    app.dependency_overrides[get_db] = override_get_db

    yield TestClient(app)

    app.dependency_overrides.clear()

# tests/test_auth.py

def test_register_user_success(client):

    response = client.post("/auth/register", json={

        "email": "test@example.com",

        "username": "testuser",

        "password": "Password123!"

    })

    assert response.status_code == 201

    assert "access_token" in response.json()

    assert response.json()["user"]["email"] == "test@example.com"

def test_register_duplicate_email(client):

    # First user

    client.post("/auth/register", json={

        "email": "test@example.com",

        "username": "user1",

        "password": "Password123!"

    })

    # Duplicate email

    response = client.post("/auth/register", json={

        "email": "test@example.com",

        "username": "user2",

        "password": "Password123!"

    })

    assert response.status_code == 409

    assert "already exists" in response.json()["detail"]

def test_login_success(client):

    # Register

    client.post("/auth/register", json={

        "email": "test@example.com",

        "username": "testuser",

        "password": "Password123!"

    })

    # Login

    response = client.post("/auth/login", json={

        "email": "test@example.com",

        "password": "Password123!"

    })

    assert response.status_code == 200

    assert "access_token" in response.json()

def test_protected_route_without_token(client):

    response = client.get("/auth/me")

    assert response.status_code == 401

def test_protected_route_with_token(client):

    # Register and get token

    register_response = client.post("/auth/register", json={

        "email": "test@example.com",

        "username": "testuser",

        "password": "Password123!"

    })

    token = register_response.json()["access_token"]

    # Access protected route

    response = client.get("/auth/me", headers={

        "Authorization": f"Bearer {token}"

    })

    assert response.status_code == 200

    assert response.json()["email"] == "test@example.com"

Best practices

Quality improvements

-

TDD (Test-Driven Development): Write tests before writing code

  • Clarifies requirements
  • Improves design
  • Naturally achieves high coverage

-

Given-When-Then pattern: Write tests in BDD style

it('should return 404 when user not found', async () => {

  // Given: a non-existent user ID

  const nonExistentId = 'non-existent-uuid';

  // When: attempting to look up that user

  const response = await request(app).get(`/users/${nonExistentId}`);

  // Then: 404 response

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

});

-

Test Fixtures: Reusable test data

const validUser = {

  email: 'test@example.com',

  username: 'testuser',

  password: 'Password123!'

};

Efficiency improvements

  • Parallel execution: Speed up tests with Jest's --maxWorkers option
  • Snapshot Testing: Save snapshots of UI components or JSON responses
  • Coverage thresholds: Enforce minimum coverage in jest.config.js

Common Issues

Issue 1: Test failures caused by shared state between tests

Symptom: Passes individually but fails when run together

Cause: DB state shared due to missing beforeEach/afterEach

Fix:

beforeEach(async () => {

  await db.migrate.rollback();

  await db.migrate.latest();

});

Issue 2: "Jest did not exit one second after the test run"

Symptom: Process does not exit after tests complete

Cause: DB connections, servers, etc. not cleaned up

Fix:

afterAll(async () => {

  await db.destroy();

  await server.close();

});

Issue 3: Async test timeout

Symptom: "Timeout - Async callback was not invoked"

Cause: Missing async/await or unhandled Promise

Fix:

// Bad

it('should work', () => {

  request(app).get('/users');  // Promise not handled

});

// Good

it('should work', async () => {

  await request(app).get('/users');

});

References

Official docs

Learning resources

Tools

  • nock - HTTP mocking

Metadata

Version

  • Current version: 1.0.0
  • Last updated: 2025-01-01
  • Compatible platforms: Claude, ChatGPT, Gemini

Related skills

Tags

#testing #backend #Jest #Pytest #unit-test #integration-test #TDD #API-test

BrowserAct

Let your agent run on any real-world website

Bypass CAPTCHA & anti-bot for free. Start local, scale to cloud.

Explore BrowserAct Skills →

Stop writing automation&scrapers

Install the CLI. Run your first Skill in 30 seconds. Scale when you're ready.

Start free
free · no credit card