backend-testing

When to use this skill

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "backend-testing" with this command: npx skills add akillness/skills-template/akillness-skills-template-backend-testing

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 (!/[!@#$%^&*]/.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: &#x3C;a href="https://example.com/verify?token=${token}">Verify&#x3C;/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 files92.588.395.292.8
auth/95.090.0100.095.0
middleware.ts95.090.0100.095.0
routes.ts95.090.0100.095.0
utils/90.085.090.090.0
password.ts90.085.090.090.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

  • Jest Documentation

  • Pytest Documentation

  • Supertest GitHub

Learning resources

  • Testing JavaScript with Kent C. Dodds

  • Test-Driven Development by Example (Kent Beck)

Tools

  • Istanbul/nyc - code coverage

  • nock - HTTP mocking

  • faker.js - test data generation

Metadata

Version

  • Current version: 1.0.0

  • Last updated: 2025-01-01

  • Compatible platforms: Claude, ChatGPT, Gemini

Related skills

  • api-design: Design APIs alongside tests

  • authentication-setup: Test authentication systems

Tags

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

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

backend-testing

When to use this skill

Repository Source
General

omc

No summary provided by upstream source.

Repository SourceNeeds Review
General

plannotator

No summary provided by upstream source.

Repository SourceNeeds Review