Claude
Skills
Sign in
Back

backend-testing

Included with Lifetime
$97 forever

Toolkit for backend API testing with proper test frameworks (vitest/jest, go test, pytest, cargo test). Supports verifying API endpoints, database state changes, error handling, and collecting test evidence. curl/httpie for testing is PROHIBITED - use proper test frameworks only. [MANDATORY] Before saying "implementation complete", you MUST use this skill to run tests and verify functionality. Completion reports without verification are PROHIBITED.

Backend & APIs

What this skill does


# Backend API Testing

To test backend APIs, use **proper test frameworks** for your language. Manual testing with curl/httpie is strictly prohibited.

**CRITICAL: Test File Placement**
- **ALWAYS** place test files in the project's standard test directory (`tests/`, `__tests__/`, `*_test.go`, etc.)
- **NEVER** place test scripts in `.artifacts/` - that's for evidence only (test output logs, coverage reports)
- Test files should be permanent project assets, not disposable artifacts

## Decision Tree: Framework Selection by Language

```
Project language → Which framework?
    │
    ├─ Node.js / TypeScript
    │   ├─ Vitest (preferred) + supertest
    │   └─ Jest + supertest (if project already uses Jest)
    │
    ├─ Go
    │   └─ go test + net/http/httptest (stdlib)
    │
    ├─ Python
    │   ├─ pytest + httpx (preferred for async)
    │   └─ pytest + requests (sync only)
    │
    └─ Rust
        └─ cargo test + actix-web::test / axum::test / reqwest
```

## Test Framework Selection Table

| Language | Framework | HTTP Client | DB Access |
|----------|-----------|-------------|-----------|
| Node.js/TS | vitest or jest | supertest | prisma / drizzle / knex |
| Go | go test | net/http/httptest | database/sql / sqlx / gorm |
| Python | pytest | httpx / TestClient | sqlalchemy / asyncpg |
| Rust | cargo test | reqwest / framework test utils | sqlx / diesel |

## Example Tests

### Node.js (Vitest + supertest)

```typescript
// tests/api/users.test.ts
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import request from 'supertest';
import { app } from '../../src/app';
import { db } from '../../src/db';

describe('POST /api/users', () => {
  beforeEach(async () => {
    await db.execute('DELETE FROM users WHERE email = ?', ['[email protected]']);
  });

  afterEach(async () => {
    await db.execute('DELETE FROM users WHERE email = ?', ['[email protected]']);
  });

  it('should create a user and return 201', async () => {
    // Act
    const res = await request(app)
      .post('/api/users')
      .send({ email: '[email protected]', name: 'Test User' })
      .expect(201);

    // Assert response body
    expect(res.body).toMatchObject({
      email: '[email protected]',
      name: 'Test User',
    });
    expect(res.body.id).toBeDefined();

    // Assert DB state changed
    const rows = await db.query('SELECT * FROM users WHERE email = ?', ['[email protected]']);
    expect(rows).toHaveLength(1);
    expect(rows[0].name).toBe('Test User');
  });

  it('should return 400 for invalid email', async () => {
    const res = await request(app)
      .post('/api/users')
      .send({ email: 'not-an-email', name: 'Test User' })
      .expect(400);

    expect(res.body.error).toContain('email');

    // Assert DB was NOT modified
    const rows = await db.query('SELECT * FROM users WHERE email = ?', ['not-an-email']);
    expect(rows).toHaveLength(0);
  });

  it('should return 409 for duplicate email', async () => {
    // Setup: create first user
    await request(app)
      .post('/api/users')
      .send({ email: '[email protected]', name: 'First User' })
      .expect(201);

    // Act: attempt duplicate
    const res = await request(app)
      .post('/api/users')
      .send({ email: '[email protected]', name: 'Duplicate User' })
      .expect(409);

    expect(res.body.error).toContain('already exists');
  });
});
```

### Go (go test + httptest)

```go
// handlers/users_test.go
package handlers_test

import (
    "bytes"
    "encoding/json"
    "net/http"
    "net/http/httptest"
    "testing"

    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/require"
    "myapp/handlers"
    "myapp/db"
)

func TestCreateUser(t *testing.T) {
    testDB := db.SetupTestDB(t)
    defer testDB.Cleanup()

    handler := handlers.NewUserHandler(testDB)

    t.Run("creates user and returns 201", func(t *testing.T) {
        body, _ := json.Marshal(map[string]string{
            "email": "[email protected]",
            "name":  "Test User",
        })

        req := httptest.NewRequest(http.MethodPost, "/api/users", bytes.NewReader(body))
        req.Header.Set("Content-Type", "application/json")
        rec := httptest.NewRecorder()

        handler.CreateUser(rec, req)

        // Assert status code
        assert.Equal(t, http.StatusCreated, rec.Code)

        // Assert response body
        var resp map[string]interface{}
        err := json.Unmarshal(rec.Body.Bytes(), &resp)
        require.NoError(t, err)
        assert.Equal(t, "[email protected]", resp["email"])

        // Assert DB state
        user, err := testDB.GetUserByEmail("[email protected]")
        require.NoError(t, err)
        assert.Equal(t, "Test User", user.Name)
    })

    t.Run("returns 400 for invalid email", func(t *testing.T) {
        body, _ := json.Marshal(map[string]string{
            "email": "not-an-email",
            "name":  "Test User",
        })

        req := httptest.NewRequest(http.MethodPost, "/api/users", bytes.NewReader(body))
        req.Header.Set("Content-Type", "application/json")
        rec := httptest.NewRecorder()

        handler.CreateUser(rec, req)

        assert.Equal(t, http.StatusBadRequest, rec.Code)
    })
}
```

### Python (pytest + httpx)

```python
# tests/test_users.py
import pytest
from httpx import AsyncClient, ASGITransport
from app.main import app
from app.db import get_session

@pytest.fixture
async def client():
    transport = ASGITransport(app=app)
    async with AsyncClient(transport=transport, base_url="http://test") as ac:
        yield ac

@pytest.fixture(autouse=True)
async def cleanup_db():
    yield
    async with get_session() as session:
        await session.execute("DELETE FROM users WHERE email = '[email protected]'")
        await session.commit()

@pytest.mark.asyncio
async def test_create_user_returns_201(client: AsyncClient):
    # Act
    response = await client.post("/api/users", json={
        "email": "[email protected]",
        "name": "Test User",
    })

    # Assert status code
    assert response.status_code == 201

    # Assert response body
    data = response.json()
    assert data["email"] == "[email protected]"
    assert data["name"] == "Test User"
    assert "id" in data

    # Assert DB state changed
    async with get_session() as session:
        result = await session.execute(
            "SELECT * FROM users WHERE email = '[email protected]'"
        )
        rows = result.fetchall()
        assert len(rows) == 1
        assert rows[0].name == "Test User"

@pytest.mark.asyncio
async def test_create_user_invalid_email_returns_400(client: AsyncClient):
    response = await client.post("/api/users", json={
        "email": "not-an-email",
        "name": "Test User",
    })

    assert response.status_code == 400
    assert "email" in response.json()["detail"]
```

### Rust (cargo test)

```rust
// tests/api/users.rs
use actix_web::test;
use actix_web::web::Data;
use myapp::{create_app, db::TestDb};

#[actix_web::test]
async fn test_create_user_returns_201() {
    let test_db = TestDb::new().await;
    let app = test::init_service(create_app(Data::new(test_db.pool.clone()))).await;

    let req = test::TestRequest::post()
        .uri("/api/users")
        .set_json(serde_json::json!({
            "email": "[email protected]",
            "name": "Test User"
        }))
        .to_request();

    let resp = test::call_service(&app, req).await;

    // Assert status code
    assert_eq!(resp.status(), 201);

    // Assert response body
    let body: serde_json::Value = test::read_body_json(resp).await;
    assert_eq!(body["email"], "[email protected]");
    assert_eq!(body["name"], "Test User");

    // Assert DB state
    let row = sqlx::query!("SELECT name FROM users WHERE email = $1", "[email protected]")
        .fetch_one(&test_db.pool)
        .await
        .unwrap();
    assert_eq!(row.name, "Test User");

    test_db.cleanup().await;
}

#[actix_web::test]
async fn tes
Files: 1
Size: 14.8 KB
Complexity: 27/100
Category: Backend & APIs

Related in Backend & APIs