К основному содержанию
скилл CX Codex

api-test-suite-generator — скилл для Claude Code: тесты API

Скилл сканирует определения маршрутов и генерирует тесты API на Jest, Vitest или Supertest. Пригодится, когда эндпоинты уже есть, а тестов к ним нет.

Установить в Claude Code · Через Skills CLI (Vercel) Все способы и платформы ↓
npx skills add patricio0312rev/skills --skill api-test-suite-generator -a claude-code
Открыть на GitHub

Что делает api-test-suite-generator

Скилл проходит по проекту и находит определения API-маршрутов: Express, Next.js, Fastify и другие. Дальше он вытаскивает схемы запросов и ответов и раскладывает их в тестовые файлы по ресурсам. На каждый маршрут получается набор проверок: успешный сценарий, коды статусов, структура ответа, заголовки. Отдельно добавляются пограничные случаи — невалидное тело, отсутствие токена, несуществующий ресурс.

Структура предсказуемая: tests/setup.ts поднимает сервер на случайном порту и чистит базу перед каждым тестом, в tests/fixtures/ лежат тестовые данные, в tests/helpers/api-client.ts — обёртка над Supertest с авторизацией через Authorization: Bearer. Есть и скрипт-генератор scripts/generate-api-tests.ts: он собирает файлы из описаний маршрутов. Примеры в скилле завязаны на Prisma — если у вас другая ORM, куски с prisma.$executeRaw и $disconnect придётся переписать под себя.

Скилл ставится в Claude, Codex, Cursor, GitHub Copilot, Gemini CLI и OpenCode. Для GitHub Copilot команда установки другая — через gh skill install. Тесты он пишет на TypeScript, так что проект без TS потребует правок в сгенерированных файлах.

Какие задачи решает

  • написать интеграционные тесты на все эндпоинты Express
  • покрыть проверками коды 401, 404 и 400
  • собрать фикстуры и сидинг базы для тестов
  • поднять тестовый сервер на случайном порту
  • добавить авторизацию в запросы Supertest

Примеры запросов агенту

  • сгенерируй тесты API для маршрутов users
  • создай интеграционные тесты для эндпоинтов /api/products
  • сделай тестовый набор для API с проверкой авторизации

Как установить api-test-suite-generator

1Через Skills CLI (Vercel)

npx skills add patricio0312rev/skills --skill api-test-suite-generator -a claude-code

2Вручную: положить файлы

~/.claude/skills/api-test-suite-generator/SKILL.md

Папка скилла целиком; для одного проекта — .claude/skills/api-test-suite-generator/

По шагам, с командами для каждой платформы: Как установить скилл в Claude Code: пять способов

Вопросы о api-test-suite-generator

Как установить api-test-suite-generator в Claude?

Выполните npx skills add patricio0312rev/skills --skill api-test-suite-generator -a claude-code. Файл скилла окажется в ~/.claude/skills/api-test-suite-generator/SKILL.md.

Что делает api-test-suite-generator?

Находит определения API-маршрутов, разбирает схемы запросов и ответов и генерирует тестовые файлы на Jest, Vitest или Supertest. В набор входят успешные сценарии, проверки статусов и пограничные случаи.

Какие фреймворки и тест-раннеры поддерживаются?

Маршруты читаются из Express, Next.js, Fastify и других. Тесты пишутся под Jest, Vitest или Supertest, примеры в скилле используют Prisma для работы с базой.

Исходный текст

SKILL.md · MIT
---
name: api-test-suite-generator
description: Generates comprehensive API test suites using Jest, Vitest, or Supertest from Express, Next.js, Fastify, or other API routes. Creates integration tests, contract tests, and edge case coverage. Use when users request "generate api tests", "create endpoint tests", "api test suite", or "integration tests for api".
---

# API Test Suite Generator

Generate comprehensive API test suites automatically from your route definitions.

## Core Workflow

1. **Scan routes**: Find all API route definitions
2. **Analyze contracts**: Extract request/response schemas
3. **Generate tests**: Create test files for each resource
4. **Add assertions**: Status codes, response structure, headers
5. **Include edge cases**: Invalid inputs, auth, not found
6. **Setup fixtures**: Test data and database seeding

## Test Structure

```
tests/
├── setup.ts              # Global test setup
├── fixtures/             # Test data
│   ├── users.ts
│   └── products.ts
├── integration/          # API integration tests
│   ├── users.test.ts
│   ├── products.test.ts
│   └── auth.test.ts
└── helpers/              # Test utilities
    ├── api-client.ts
    └── auth.ts
```

## Test Setup (Vitest/Jest)

```typescript
// tests/setup.ts
import { beforeAll, afterAll, beforeEach, afterEach } from "vitest";
import { createServer } from "../src/server";
import { prisma } from "../src/db";

let server: ReturnType<typeof createServer>;

beforeAll(async () => {
  server = await createServer();
  await server.listen({ port: 0 }); // Random port
  process.env.TEST_BASE_URL = `http://localhost:${server.address().port}`;
});

afterAll(async () => {
  await server.close();
  await prisma.$disconnect();
});

beforeEach(async () => {
  // Clean database before each test
  await prisma.$executeRaw`TRUNCATE TABLE users CASCADE`;
});

afterEach(async () => {
  // Cleanup after each test
});

export { server };
```

## API Test Client

```typescript
// tests/helpers/api-client.ts
import supertest from "supertest";

const baseUrl = process.env.TEST_BASE_URL || "http://localhost:3000";

export const api = supertest(baseUrl);

export async function authenticatedApi(token?: string) {
  const authToken = token || (await getTestAuthToken());
  return {
    get: (url: string) => api.get(url).set("Authorization", `Bearer ${authToken}`),
    post: (url: string) => api.post(url).set("Authorization", `Bearer ${authToken}`),
    put: (url: string) => api.put(url).set("Authorization", `Bearer ${authToken}`),
    patch: (url: string) => api.patch(url).set("Authorization", `Bearer ${authToken}`),
    delete: (url: string) => api.delete(url).set("Authorization", `Bearer ${authToken}`),
  };
}

async function getTestAuthToken(): Promise<string> {
  const response = await api.post("/api/auth/login").send({
    email: "[email protected]",
    password: "testpassword",
  });
  return response.body.token;
}
```

## Test Generator Script

```typescript
// scripts/generate-api-tests.ts
import * as fs from "fs";
import * as path from "path";

interface RouteInfo {
  method: string;
  path: string;
  name: string;
  params?: { name: string; type: "path" | "query" }[];
  requestBody?: object;
  responseSchema?: object;
  auth?: boolean;
}

interface TestCase {
  name: string;
  description: string;
  method: string;
  path: string;
  body?: object;
  expectedStatus: number;
  expectedBody?: object;
  headers?: Record<string, string>;
  auth?: boolean;
}

function generateTestFile(
  resource: string,
  routes: RouteInfo[]
): string {
  const lines: string[] = [];

  // Imports
  lines.push(`import { describe, it, expect, beforeEach, afterEach } from "vitest";`);
  lines.push(`import { api, authenticatedApi } from "../helpers/api-client";`);
  lines.push(`import { create${capitalize(resource)} } from "../fixtures/${resource}";`);
  lines.push("");

  // Test suite
  lines.push(`describe("${capitalize(resource)} API", () => {`);

  for (const route of routes) {
    const testCases = generateTestCases(route);

    lines.push(`  describe("${route.method} ${route.path}", () => {`);

    for (const testCase of testCases) {
      lines.push(generateTestCase(testCase, route));
    }

    lines.push(`  });`);
    lines.push("");
  }

  lines.push(`});`);

  return lines.join("\n");
}

function generateTestCases(route: RouteInfo): TestCase[] {
  const cases: TestCase[] = [];

  // Success case
  cases.push({
    name: `should ${getActionVerb(route.method)} successfully`,
    description: `Happy path for ${route.method} ${route.path}`,
    method: route.method,
    path: route.path,
    body: route.requestBody,
    expectedStatus: getExpectedStatus(route.method),
    auth: route.auth,
  });

  // Auth failure case (if auth required)
  if (route.auth) {
    cases.push({
      name: "should return 401 without auth token",
      description: "Unauthorized access attempt",
      method: route.method,
      path: route.path,
      expectedStatus: 401,
      auth: false,
    });
  }

  // Not found case (if has path params)
  if (route.params?.some((p) => p.type === "path")) {
    cases.push({
      name: "should return 404 for non-existent resource",
      description: "Resource not found",
      method: route.method,
      path: route.path.replace(/:(\w+)/g, "non-existent-id"),
      expectedStatus: 404,
      auth: route.auth,
    });
  }

  // Validation error case (for POST/PUT/PATCH)
  if (["POST", "PUT", "PATCH"].includes(route.method)) {
    cases.push({
      name: "should return 400 for invalid request body",
      description: "Validation failure",
      method: route.method,
      path: route.path,
      body: {},
      expectedStatus: 400,
      auth: route.auth,
    });
  }

  return cases;
}

function generateTestCase(testCase: TestCase, route: RouteInfo): string {
  const lines: string[] = [];
  const indent = "    ";

  lines.push(`${indent}it("${testCase.name}", async () => {`);

  // Setup
  if (route.params?.some((p) => p.type === "path")) {
    lines.push(`${indent}  // Setup: Create test resource`);
    lines.push(`${indent}  const resource = await createTestResource();`);
    lines.push(`${indent}  const url = "${route.path}".replace(":id", resource.id);`);
  } else {
    lines.push(`${indent}  const url = "${route.path}";`);
  }

  // Make request
  lines.push("");
  if (testCase.auth) {
    lines.push(`${indent}  const client = await authenticatedApi();`);
    lines.push(
      `${indent}  const response = await client.${testCase.method.toLowerCase()}(url)`
    );
  } else {
    lines.push(
      `${indent}  const response = await api.${testCase.method.toLowerCase()}(url)`
    );
  }

  if (testCase.body) {
    lines.push(`${indent}    .send(${JSON.stringify(testCase.body, null, 2).replace(/\n/g, `\n${indent}    `)})`);
  }

  lines.push(`${indent}    .expect(${testCase.expectedStatus});`);

  // Assertions
  lines.push("");
  if (testCase.expectedStatus < 400) {
    lines.push(`${indent}  expect(response.body).toBeDefined();`);
    if (testCase.method === "POST") {
      lines.push(`${indent}  expect(response.body.id).toBeDefined();`);
    }
  } else {
    lines.push(`${indent}  expect(response.body.error).toBeDefined();`);
  }

  lines.push(`${indent}});`);
  lines.push("");

  return lines.join("\n");
}

function getActionVerb(method: string): string {
  const verbs: Record<string, string> = {
    GET: "retrieve",
    POST: "create",
    PUT: "update",
    PATCH: "partially update",
    DELETE: "delete",
  };
  return verbs[method] || "process";
}

function getExpectedStatus(method: string): number {
  const statuses: Record<string, number> = {
    GET: 200,
    POST: 201,
    PUT: 200,
    PATCH: 200,
    DELETE: 204,
  };
  return statuses[method] || 200;
}

function capitalize(str: string): string {
  return str.charAt(0).toUpperCase() + str.slice(1);
}
```

## Example Generated Tests

```typescript
// tests/integration/users.test.ts
import { describe, it, expect, beforeEach } from "vitest";
import { api, authenticatedApi } from "../helpers/api-client";
import { createUser, createUsers } from "../fixtures/users";

describe("Users API", () => {
  describe("GET /api/users", () => {
    it("should return paginated list of users", async () => {
      // Setup
      await createUsers(15);

      // Request
      const client = await authenticatedApi();
      const response = await client
        .get("/api/users")
        .query({ page: 1, limit: 10 })
        .expect(200);

      // Assertions
      expect(response.body.success).toBe(true);
      expect(response.body.data).toHaveLength(10);
      expect(response.body.meta.total).toBe(15);
      expect(response.body.meta.page).toBe(1);
      expect(response.body.meta.total_pages).toBe(2);
    });

    it("should return 401 without auth token", async () => {
      const response = await api.get("/api/users").expect(401);

      expect(response.body.error.code).toBe("UNAUTHORIZED");
    });
  });

  describe("GET /api/users/:id", () => {
    it("should return user by ID", async () => {
      const user = await createUser({ name: "Test User" });

      const client = await authenticatedApi();
      const response = await client
        .get(`/api/users/${user.id}`)
        .expect(200);

      expect(response.body.data.id).toBe(user.id);
      expect(response.body.data.name).toBe("Test User");
    });

    it("should return 404 for non-existent user", async () => {
      const client = await authenticatedApi();
      const response = await client
        .get("/api/users/non-existent-id")
        .expect(404);

      expect(response.body.error.code).toBe("NOT_FOUND");
    });
  });

  describe("POST /api/users", () => {
    it("should create new user", async () => {
      const client = await authenticatedApi();
      const response = await client
        .post("/api/users")
        .send({
          name: "New User",
          email: "[email protected]",
          role: "user",
        })
        .expect(201);

      expect(response.body.data.id).toBeDefined();
      expect(response.body.data.name).toBe("New User");
      expect(response.body.data.email).toBe("[email protected]");
    });

    it("should return 400 for invalid request body", async () => {
      const client = await authenticatedApi();
      const response = await client
        .post("/api/users")
        .send({})
        .expect(400);

      expect(response.body.error.code).toBe("VALIDATION_ERROR");
      expect(response.body.error.details).toBeDefined();
    });

    it("should return 409 for duplicate email", async () => {
      await createUser({ email: "[email protected]" });

      const client = await authenticatedApi();
      const response = await client
        .post("/api/users")
        .send({
          name: "New User",
          email: "[email protected]",
        })
        .expect(409);

      expect(response.body.error.code).toBe("CONFLICT");
    });
  });

  describe("PUT /api/users/:id", () => {
    it("should update user", async () => {
      const user = await createUser({ name: "Original Name" });

      const client = await authenticatedApi();
      const response = await client
        .put(`/api/users/${user.id}`)
        .send({
          name: "Updated Name",
          email: user.email,
        })
        .expect(200);

      expect(response.body.data.name).toBe("Updated Name");
    });

    it("should return 404 for non-existent user", async () => {
      const client = await authenticatedApi();
      const response = await client
        .put("/api/users/non-existent-id")
        .send({ name: "Test" })
        .expect(404);

      expect(response.body.error.code).toBe("NOT_FOUND");
    });
  });

  describe("DELETE /api/users/:id", () => {
    it("should delete user", async () => {
      const user = await createUser();

      const client = await authenticatedApi();
      await client.delete(`/api/users/${user.id}`).expect(204);

      // Verify deletion
      await client.get(`/api/users/${user.id}`).expect(404);
    });

    it("should return 404 for non-existent user", async () => {
      const client = await authenticatedApi();
      await client.delete("/api/users/non-existent-id").expect(404);
    });
  });
});
```

## Test Fixtures

```typescript
// tests/fixtures/users.ts
import { prisma } from "../../src/db";
import { faker } from "@faker-js/faker";

interface CreateUserOptions {
  name?: string;
  email?: string;
  role?: string;
}

export async function createUser(options: CreateUserOptions = {}) {
  return prisma.user.create({
    data: {
      name: options.name ?? faker.person.fullName(),
      email: options.email ?? faker.internet.email(),
      role: options.role ?? "user",
      password: await hashPassword("testpassword"),
    },
  });
}

export async function createUsers(count: number) {
  const users = Array.from({ length: count }, () => ({
    name: faker.person.fullName(),
    email: faker.internet.email(),
    role: "user",
    password: "hashed-password",
  }));

  return prisma.user.createMany({ data: users });
}

export async function createAdminUser() {
  return createUser({ role: "admin" });
}
```

## Contract Testing

```typescript
// tests/contract/users.contract.test.ts
import { describe, it, expect } from "vitest";
import { api, authenticatedApi } from "../helpers/api-client";
import { z } from "zod";

// Response schemas
const UserSchema = z.object({
  id: z.string().uuid(),
  name: z.string(),
  email: z.string().email(),
  role: z.enum(["user", "admin"]),
  createdAt: z.string().datetime(),
});

const PaginatedUsersSchema = z.object({
  success: z.literal(true),
  data: z.array(UserSchema),
  meta: z.object({
    page: z.number(),
    limit: z.number(),
    total: z.number(),
    total_pages: z.number(),
  }),
});

describe("Users API Contract", () => {
  it("GET /api/users should match contract", async () => {
    const client = await authenticatedApi();
    const response = await client.get("/api/users").expect(200);

    // Validate against schema
    const result = PaginatedUsersSchema.safeParse(response.body);
    expect(result.success).toBe(true);
  });

  it("GET /api/users/:id should match contract", async () => {
    const user = await createUser();

    const client = await authenticatedApi();
    const response = await client.get(`/api/users/${user.id}`).expect(200);

    // Validate against schema
    const result = UserSchema.safeParse(response.body.data);
    expect(result.success).toBe(true);
  });
});
```

## CLI Script

```typescript
#!/usr/bin/env node
// scripts/test-gen.ts
import * as fs from "fs";
import * as path from "path";
import { program } from "commander";

program
  .name("test-gen")
  .description("Generate API test suite from routes")
  .option("-f, --framework <type>", "Framework (express|nextjs|fastify)", "express")
  .option("-s, --source <path>", "Source directory", "./src")
  .option("-o, --output <path>", "Output directory", "./tests/integration")
  .option("-t, --test-runner <type>", "Test runner (vitest|jest)", "vitest")
  .parse();

const options = program.opts();

async function main() {
  const routes = await scanRoutes(options.framework, options.source);
  const groupedRoutes = groupRoutesByResource(routes);

  if (!fs.existsSync(options.output)) {
    fs.mkdirSync(options.output, { recursive: true });
  }

  for (const [resource, resourceRoutes] of Object.entries(groupedRoutes)) {
    const content = generateTestFile(resource, resourceRoutes);
    const filePath = path.join(options.output, `${resource}.test.ts`);
    fs.writeFileSync(filePath, content);
    console.log(`Generated ${filePath}`);
  }
}

main();
```

## Best Practices

1. **Isolate tests**: Each test should be independent
2. **Clean state**: Reset database between tests
3. **Use fixtures**: Create reusable test data factories
4. **Test edge cases**: Invalid input, auth, not found
5. **Contract testing**: Validate response schemas
6. **Descriptive names**: Tests should read like documentation
7. **Fast execution**: Use transactions for database cleanup
8. **CI integration**: Run tests on every PR

## Output Checklist

- [ ] Test setup with server lifecycle
- [ ] API client helper with auth support
- [ ] Test files for each resource
- [ ] Happy path tests for all endpoints
- [ ] Authentication failure tests
- [ ] Validation error tests
- [ ] Not found tests for path params
- [ ] Conflict/duplicate tests where applicable
- [ ] Contract/schema validation tests
- [ ] Test fixtures for each resource

© patricio0312rev · MIT · patricio0312rev/skills

Похожие скиллы

Всё по теме «Тесты и QA» →
test-automator zhaono1 Помогает писать юнит-, интеграционные и E2E-тесты, поднимать фреймворк и считать покрытие. Пригодится, когда тестов в проекте мало или они разъехались. CX Codex +1 812установок api-testing secondsky Скилл учит агента писать тесты HTTP API: Supertest в TypeScript, httpx и pytest в Python. Пригодится, когда нужно закрыть REST, GraphQL и проверку ошибок. CX Codex +1 890установок test-coverage cognitedata Скилл находит пробелы в покрытии тестами приложения на Flows и закрывает их: настраивает инструменты, пишет недостающие тесты, правит код ради тестируемости. CX Codex +1 1 900установок parallel-test-fixing spencerpauly Скилл распределяет упавшие файлы тестов между параллельными субагентами, чтобы исправлять ошибки независимо. Он нужен при поломке сразу нескольких тестовых файлов. CX Codex +1 412установок test-guard amElnagdy Проверяет сгенерированные тесты на избыточность, лишние моки и совпадение с логикой проекта перед коммитом. Пригодится, когда ИИ пишет тесты для pytest, Jest, Vitest, Go или PHPUnit. CX Codex +1 4 100установок dx-devops-test-suite-assignments-configure forcedotcom Подбирает тест-наборы под конкретный коммит и раскладывает их по стадиям пайплайна DevOps Center. Пригодится, когда непонятно, что запускать после правок. CX Codex +1 5 000установок

Из того же набора26

mermaid-diagram-generator Скилл рисует диаграммы Mermaid прямо в markdown: блок-схемы, sequence, ERD, состояния и классы. Пригодится, когда нужно показать поток запроса или схему базы в документации. CX Codex +1 331установок visual-regression-tester Скилл настраивает визуальное регрессионное тестирование: снимает скриншоты, сравнивает их с базовыми и ловит случайные изменения интерфейса. CX Codex +1 330установок form-wizard-builder Скилл собирает многошаговые формы: схемы валидации, шаги, общее состояние, индикатор прогресса и шаг проверки перед отправкой. CX Codex +1 328установок skill-creator Скилл, который пишет новые скиллы по конвенциям репозитория: структура, frontmatter, воркфлоу и примеры кода. CX Codex +1 321установок cost-latency-optimizer Скилл разбирает, куда уходят деньги и время в LLM-приложении, и предлагает кеш, выбор модели, батчинг и сжатие промптов. CX Codex +1 322установок rate-limiting-abuse-protection Скилл добавляет в проект ограничение частоты запросов: политики на маршрут, лимиты по IP и пользователю, скользящее окно и понятные ответы об ошибке. CX Codex +1 319установок load-test-builder Собирает нагрузочные и стресс-тесты на k6, Artillery или autocannon и помогает встроить их в CI как проверку производительности. CX Codex +1 316установок openapi-generator Собирает спецификацию OpenAPI 3.0/3.1 из маршрутов Express, Next.js, Fastify, Hono или NestJS — со схемами, примерами и описаниями. CX Codex +1 306установок

Спонсоры проекта

Компании, которые помогают держать аналитику открытой