Claude Code Plugins

Community-maintained marketplace

Feedback

Write tests for backend services, APIs, and database access. Use when testing Express/Fastify handlers, services with database calls, or integration tests.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name backend-testing
description Write tests for backend services, APIs, and database access. Use when testing Express/Fastify handlers, services with database calls, or integration tests.
allowed-tools Read, Write, Edit, Bash, Glob, Grep

Backend Testing Skill

Write and maintain tests for backend services using Node.js native test runner with patterns for database mocking, API testing, and integration tests.

Test Categories

Type Purpose Database Speed
Unit Service logic in isolation Mocked Fast
Integration Service + real database Test DB Medium
API HTTP endpoints Mocked or Test DB Medium
E2E Full request flow Test DB Slow

Directory Structure

test/
├── unit/
│   └── services/
│       └── user.test.js
├── integration/
│   └── services/
│       └── user.integration.test.js
├── api/
│   └── auth.api.test.js
├── helpers/
│   ├── db.js          # Test database utilities
│   ├── request.js     # HTTP request helpers
│   └── mocks.js       # Common mocks
└── fixtures/
    └── users.json

Unit Testing Services

Mocking Database Access

import { describe, it, beforeEach } from 'node:test';
import assert from 'node:assert';
import { mock } from 'node:test';

// Service under test
import { UserService } from '../../src/services/user.js';

describe('UserService', () => {
  let mockDb;
  let service;

  beforeEach(() => {
    // Create mock database client
    mockDb = {
      query: mock.fn()
    };
    service = new UserService(mockDb);
  });

  describe('findById', () => {
    it('should return user when found', async () => {
      const mockUser = { id: '123', email: 'test@example.com' };
      mockDb.query.mock.mockImplementation(() => ({
        rows: [mockUser]
      }));

      const result = await service.findById('123');

      assert.deepStrictEqual(result, mockUser);
      assert.strictEqual(mockDb.query.mock.calls.length, 1);
    });

    it('should return null when not found', async () => {
      mockDb.query.mock.mockImplementation(() => ({ rows: [] }));

      const result = await service.findById('nonexistent');

      assert.strictEqual(result, null);
    });

    it('should throw on database error', async () => {
      mockDb.query.mock.mockImplementation(() => {
        throw new Error('Connection failed');
      });

      await assert.rejects(
        () => service.findById('123'),
        { message: 'Connection failed' }
      );
    });
  });
});

Service with Dependencies

import { describe, it, beforeEach } from 'node:test';
import { mock } from 'node:test';

describe('OrderService', () => {
  let mockDb;
  let mockEmailService;
  let mockPaymentService;
  let service;

  beforeEach(() => {
    mockDb = { query: mock.fn() };
    mockEmailService = { send: mock.fn() };
    mockPaymentService = { charge: mock.fn() };

    service = new OrderService({
      db: mockDb,
      email: mockEmailService,
      payment: mockPaymentService
    });
  });

  it('should create order and send confirmation', async () => {
    mockDb.query.mock.mockImplementation(() => ({
      rows: [{ id: 'order-1' }]
    }));
    mockPaymentService.charge.mock.mockImplementation(() => ({
      success: true
    }));

    await service.create({ userId: '123', items: [] });

    assert.strictEqual(mockEmailService.send.mock.calls.length, 1);
    assert.strictEqual(
      mockEmailService.send.mock.calls[0].arguments[0].type,
      'order_confirmation'
    );
  });
});

Integration Testing with Test Database

Test Database Setup

// test/helpers/db.js
import pg from 'pg';

const TEST_DATABASE_URL = process.env.TEST_DATABASE_URL
  || 'postgresql://localhost:5432/myapp_test';

let pool;

/**
 * Get or create test database pool
 * @returns {pg.Pool}
 */
export function getTestPool() {
  if (!pool) {
    pool = new pg.Pool({ connectionString: TEST_DATABASE_URL });
  }
  return pool;
}

/**
 * Run migrations on test database
 */
export async function migrateTestDb() {
  const { migrate } = await import('../../src/db/migrate.js');
  await migrate(getTestPool());
}

/**
 * Truncate all tables (preserves schema)
 */
export async function truncateAll() {
  const pool = getTestPool();
  await pool.query(`
    DO $$ DECLARE
      r RECORD;
    BEGIN
      FOR r IN (SELECT tablename FROM pg_tables WHERE schemaname = 'public' AND tablename != 'migrations') LOOP
        EXECUTE 'TRUNCATE TABLE ' || quote_ident(r.tablename) || ' CASCADE';
      END LOOP;
    END $$;
  `);
}

/**
 * Close test database pool
 */
export async function closeTestDb() {
  if (pool) {
    await pool.end();
    pool = null;
  }
}

/**
 * Insert fixture data
 * @param {string} table
 * @param {Object[]} rows
 */
export async function insertFixtures(table, rows) {
  const pool = getTestPool();
  for (const row of rows) {
    const keys = Object.keys(row);
    const values = Object.values(row);
    const placeholders = keys.map((_, i) => `$${i + 1}`).join(', ');
    await pool.query(
      `INSERT INTO ${table} (${keys.join(', ')}) VALUES (${placeholders})`,
      values
    );
  }
}

Integration Test Example

// test/integration/services/user.integration.test.js
import { describe, it, before, after, beforeEach } from 'node:test';
import assert from 'node:assert';
import {
  getTestPool,
  migrateTestDb,
  truncateAll,
  closeTestDb,
  insertFixtures
} from '../../helpers/db.js';
import { UserService } from '../../../src/services/user.js';

describe('UserService (integration)', () => {
  let service;

  before(async () => {
    await migrateTestDb();
    service = new UserService(getTestPool());
  });

  beforeEach(async () => {
    await truncateAll();
  });

  after(async () => {
    await closeTestDb();
  });

  describe('create', () => {
    it('should insert user into database', async () => {
      const user = await service.create({
        email: 'test@example.com',
        name: 'Test User',
        password: 'hashed_password'
      });

      assert.ok(user.id);
      assert.strictEqual(user.email, 'test@example.com');

      // Verify in database
      const pool = getTestPool();
      const result = await pool.query(
        'SELECT * FROM users WHERE id = $1',
        [user.id]
      );
      assert.strictEqual(result.rows.length, 1);
    });

    it('should reject duplicate email', async () => {
      await insertFixtures('users', [{
        id: '550e8400-e29b-41d4-a716-446655440000',
        email: 'existing@example.com',
        name: 'Existing',
        password_hash: 'hash',
        role: 'user',
        created_at: new Date(),
        updated_at: new Date()
      }]);

      await assert.rejects(
        () => service.create({
          email: 'existing@example.com',
          name: 'New User',
          password: 'password'
        }),
        { code: 'EMAIL_EXISTS' }
      );
    });
  });
});

API Endpoint Testing

HTTP Request Helper

// test/helpers/request.js

/**
 * Create test HTTP client for Express app
 * @param {Express} app
 * @returns {Object}
 */
export function createTestClient(app) {
  let server;
  let baseUrl;

  return {
    async start() {
      return new Promise((resolve) => {
        server = app.listen(0, () => {
          const { port } = server.address();
          baseUrl = `http://localhost:${port}`;
          resolve();
        });
      });
    },

    async stop() {
      return new Promise((resolve) => {
        server?.close(resolve);
      });
    },

    async request(method, path, options = {}) {
      const url = `${baseUrl}${path}`;
      const response = await fetch(url, {
        method,
        headers: {
          'Content-Type': 'application/json',
          ...options.headers
        },
        body: options.body ? JSON.stringify(options.body) : undefined
      });

      return {
        status: response.status,
        headers: Object.fromEntries(response.headers),
        body: await response.json().catch(() => null)
      };
    },

    get(path, options) { return this.request('GET', path, options); },
    post(path, options) { return this.request('POST', path, options); },
    patch(path, options) { return this.request('PATCH', path, options); },
    delete(path, options) { return this.request('DELETE', path, options); }
  };
}

API Test Example

// test/api/auth.api.test.js
import { describe, it, before, after, beforeEach } from 'node:test';
import assert from 'node:assert';
import { createApp } from '../../src/index.js';
import { createTestClient } from '../helpers/request.js';
import { migrateTestDb, truncateAll, closeTestDb } from '../helpers/db.js';

describe('Auth API', () => {
  let client;
  let app;

  before(async () => {
    await migrateTestDb();
    app = createApp({ isTest: true });
    client = createTestClient(app);
    await client.start();
  });

  beforeEach(async () => {
    await truncateAll();
  });

  after(async () => {
    await client.stop();
    await closeTestDb();
  });

  describe('POST /api/auth/register', () => {
    it('should create user and return tokens', async () => {
      const res = await client.post('/api/auth/register', {
        body: {
          email: 'new@example.com',
          name: 'New User',
          password: 'SecurePass123'
        }
      });

      assert.strictEqual(res.status, 201);
      assert.ok(res.body.user.id);
      assert.strictEqual(res.body.user.email, 'new@example.com');
      assert.ok(res.body.accessToken);
      assert.ok(res.body.refreshToken);
    });

    it('should reject weak password', async () => {
      const res = await client.post('/api/auth/register', {
        body: {
          email: 'new@example.com',
          name: 'New User',
          password: '123'
        }
      });

      assert.strictEqual(res.status, 400);
      assert.strictEqual(res.body.error.code, 'VALIDATION_ERROR');
    });

    it('should reject duplicate email', async () => {
      // First registration
      await client.post('/api/auth/register', {
        body: {
          email: 'dupe@example.com',
          name: 'First',
          password: 'SecurePass123'
        }
      });

      // Duplicate attempt
      const res = await client.post('/api/auth/register', {
        body: {
          email: 'dupe@example.com',
          name: 'Second',
          password: 'SecurePass123'
        }
      });

      assert.strictEqual(res.status, 409);
    });
  });

  describe('POST /api/auth/login', () => {
    beforeEach(async () => {
      await client.post('/api/auth/register', {
        body: {
          email: 'user@example.com',
          name: 'Test User',
          password: 'SecurePass123'
        }
      });
    });

    it('should return tokens for valid credentials', async () => {
      const res = await client.post('/api/auth/login', {
        body: {
          email: 'user@example.com',
          password: 'SecurePass123'
        }
      });

      assert.strictEqual(res.status, 200);
      assert.ok(res.body.accessToken);
    });

    it('should reject invalid password', async () => {
      const res = await client.post('/api/auth/login', {
        body: {
          email: 'user@example.com',
          password: 'WrongPassword'
        }
      });

      assert.strictEqual(res.status, 401);
    });
  });
});

Authenticated Request Testing

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

  beforeEach(async () => {
    await truncateAll();

    // Create and login test user
    await client.post('/api/auth/register', {
      body: {
        email: 'auth@example.com',
        name: 'Auth User',
        password: 'SecurePass123'
      }
    });

    const loginRes = await client.post('/api/auth/login', {
      body: { email: 'auth@example.com', password: 'SecurePass123' }
    });
    authToken = loginRes.body.accessToken;
  });

  it('should access protected route with token', async () => {
    const res = await client.get('/api/auth/me', {
      headers: { Authorization: `Bearer ${authToken}` }
    });

    assert.strictEqual(res.status, 200);
    assert.strictEqual(res.body.user.email, 'auth@example.com');
  });

  it('should reject request without token', async () => {
    const res = await client.get('/api/auth/me');

    assert.strictEqual(res.status, 401);
  });
});

Mocking External Services

HTTP Client Mocking

import { mock } from 'node:test';

describe('PaymentService', () => {
  let originalFetch;

  beforeEach(() => {
    originalFetch = global.fetch;
    global.fetch = mock.fn();
  });

  afterEach(() => {
    global.fetch = originalFetch;
  });

  it('should call payment provider API', async () => {
    global.fetch.mock.mockImplementation(() =>
      Promise.resolve({
        ok: true,
        json: () => Promise.resolve({ transaction_id: 'txn_123' })
      })
    );

    const result = await paymentService.charge({
      amount: 1000,
      currency: 'USD'
    });

    assert.strictEqual(result.transactionId, 'txn_123');
    assert.strictEqual(global.fetch.mock.calls.length, 1);

    const [url, options] = global.fetch.mock.calls[0].arguments;
    assert.ok(url.includes('/charges'));
    assert.strictEqual(options.method, 'POST');
  });
});

Test Configuration

package.json Scripts

{
  "scripts": {
    "test": "node --test",
    "test:unit": "node --test test/unit/**/*.test.js",
    "test:integration": "node --test test/integration/**/*.test.js",
    "test:api": "node --test test/api/**/*.test.js",
    "test:coverage": "node --test --experimental-test-coverage",
    "test:watch": "node --test --watch"
  }
}

Environment Variables

# .env.test
NODE_ENV=test
TEST_DATABASE_URL=postgresql://localhost:5432/myapp_test
LOG_LEVEL=error
JWT_SECRET=test-secret-key

Test Checklist

Before completing a backend feature:

  • Unit tests for service methods with mocked dependencies
  • Integration tests for database operations
  • API tests for all endpoints
  • Error case coverage (validation, auth, not found)
  • Edge cases (empty results, pagination bounds)
  • Authentication/authorization checks tested

Running Tests

# All tests
npm test

# By category
npm run test:unit
npm run test:integration
npm run test:api

# Single file
node --test test/api/auth.api.test.js

# With coverage
npm run test:coverage

# Watch mode during development
npm run test:watch

Related Skills