Token导航 LogoToken导航TokenDH.com
研究检索敏感数据github未标认证来源可访问许可证需确认审计通过

api-testingAPI 测试

Agent Skill

用于辅助 API 设计、接口文档、请求响应结构和服务集成说明。它适合让 Agent 梳理 endpoint、生成 OpenAPI 草稿、检查字段命名、整理错误码或辅助前后端联调。使用时需要确认真实业务语义、鉴权方式、分页和错误处理规则;涉及生成接口文档时,应避免凭空补字段,最好从现有代码、schema 或接口样例中提取事实。

总安装

336

周安装

14

GitHub Stars

4

下载量

112
CodexClaudeCursorGemini CLI

安装说明

本站只整理中文说明和来源信息,不托管安装包,也不代用户安装。

GitHub

来源数

2

许可证

unknown

最后核验

2026-05-01

来源状态

来源可访问

安装方式

通过对话安装

复制提示词发给支持本地命令或 Skills 的 AI 助手,先确认命令和权限,再让它执行。

请帮我安装这个 Agent Skill:api-testing(API 测试)
来源仓库:https://github.com/petrkindlmann/qa-skills
仓库路径:skills/api-testing
安装命令:
npx skills add https://github.com/petrkindlmann/qa-skills --skill api-testing
安装前请先检查当前环境是否支持对应 CLI,并向我确认将要执行的命令、安装目录、联网范围和文件读写权限;确认后再执行。

命令行安装

复制命令到本机终端执行。该命令会通过 npx skills 从第三方来源获取 Skill;本站只展示命令,不托管安装包,也不自动执行。

skills.shnpx skills
npx skills add https://github.com/petrkindlmann/qa-skills --skill api-testing

简介

用于辅助 API 设计、接口文档编写和错误码整理,适合前后端联调。

  • 可帮助梳理 endpoint、生成 OpenAPI 草稿并检查字段命名。
  • 通过 GitHub 安装,支持 Codex、Claude、Cursor、Gemini CLI 等宿主环境。
  • 需从现有代码或接口样例提取事实,避免虚构字段;涉及鉴权与分页时应确认真实业务规则。
  • api-testing 属于研究检索类 Skill,可作为该场景下的辅助能力补充。

SKILL.md

Discovery Questions

  1. REST, GraphQL, or both? REST-only suites use standard HTTP assertions. GraphQL needs query/mutation builders.
  2. Auth mechanism? JWT, API key, OAuth 2.0, or session cookies -- each needs a different fixture strategy.
  3. OpenAPI/Swagger spec available? If yes, auto-generate schemas as contracts.
  4. Check .agents/qa-project-context.md first. Respect existing conventions.

Core Principles

  1. Test contracts, not implementations. Assert on response shape, status codes, and headers -- not on internal logic or database state.
  2. Schema validation catches drift before it breaks consumers. A failing schema test means you caught a breaking change before your frontend did.
  3. Auth flows are tests too -- don't just hardcode tokens. Test login, refresh, expiration, and permission boundaries.
  4. Response time is a testable assertion. Performance regressions caught in CI are cheaper than production incidents.

Playwright API Testing

APIRequestContext supports standalone API tests without launching a browser and shares cookie/storage state with browser contexts.

Configuration and Standalone Tests

// playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  testDir: './api-tests',
  use: {
    baseURL: process.env.API_BASE_URL ?? 'http://localhost:3000',
    extraHTTPHeaders: { 'Accept': 'application/json' },
  },
});
import { test, expect } from '@playwright/test';

test.describe('Users API', () => {
  test('GET /api/users returns a list', async ({ request }) => {
    const response = await request.get('/api/users');
    expect(response.status()).toBe(200);
    expect(response.headers()['content-type']).toContain('application/json');

    const body = await response.json();
    expect(body.users).toBeInstanceOf(Array);
    expect(body.users[0]).toHaveProperty('id');
    expect(body.users[0]).toHaveProperty('email');
  });

  test('GET /api/users/:id returns 404 for missing user', async ({ request }) => {
    const response = await request.get('/api/users/non-existent-id');
    expect(response.status()).toBe(404);
  });
});

Combined Browser + API Tests

test('project created via API appears in dashboard', async ({ request, page }) => {
  const createRes = await request.post('/api/projects', {
    data: { name: 'API-Created Project', description: 'Seeded via API' },
  });
  expect(createRes.ok()).toBeTruthy();
  const project = await createRes.json();

  await page.goto('/dashboard');
  await expect(page.getByText('API-Created Project')).toBeVisible();

  await request.delete(`/api/projects/${project.id}`); // cleanup
});

Authenticated API Fixture

// fixtures/api.fixture.ts
import { test as base, expect, APIRequestContext } from '@playwright/test';

export const test = base.extend<{ authedApi: APIRequestContext }>({
  authedApi: async ({ playwright }, use) => {
    const api = await playwright.request.newContext({
      baseURL: process.env.API_BASE_URL ?? 'http://localhost:3000',
      extraHTTPHeaders: { 'Accept': 'application/json' },
    });
    const loginRes = await api.post('/api/auth/login', {
      data: { email: process.env.TEST_USER_EMAIL!, password: process.env.TEST_USER_PASSWORD! },
    });
    expect(loginRes.ok()).toBeTruthy();
    await use(api);
    await api.dispose();
  },
});
export { expect };

Schema Validation

Zod

import { z } from 'zod';
import { test, expect } from '@playwright/test';

const UserSchema = z.object({
  id: z.string().uuid(),
  email: z.string().email(),
  name: z.string().min(1),
  role: z.enum(['admin', 'member', 'viewer']),
  createdAt: z.string().datetime(),
});

const UsersListSchema = z.object({
  users: z.array(UserSchema),
  total: z.number().int().nonneg(),
  page: z.number().int().positive(),
  pageSize: z.number().int().positive(),
});

test('GET /api/users matches schema', async ({ request }) => {
  const response = await request.get('/api/users');
  const result = UsersListSchema.safeParse(await response.json());
  if (!result.success) console.error('Schema errors:', result.error.issues);
  expect(result.success).toBe(true);
});

AJV with JSON Schema

import Ajv from 'ajv';
import addFormats from 'ajv-formats';

const ajv = new Ajv({ allErrors: true });
addFormats(ajv);

const userSchema = {
  type: 'object',
  required: ['id', 'email', 'name', 'role'],
  properties: {
    id: { type: 'string', format: 'uuid' },
    email: { type: 'string', format: 'email' },
    name: { type: 'string', minLength: 1 },
    role: { type: 'string', enum: ['admin', 'member', 'viewer'] },
  },
  additionalProperties: false,
};

test('GET /api/users/:id conforms to JSON Schema', async ({ request }) => {
  const body = await (await request.get('/api/users/some-valid-id')).json();
  expect(ajv.compile(userSchema)(body)).toBe(true);
});

Schema-as-Contract Pattern

Both API and tests import the same schema file. If the response shape changes, consumer tests fail immediately. With an OpenAPI spec, auto-generate via json-schema-to-zod.

// shared/schemas/user.schema.ts  (imported by both API and tests)
import { z } from 'zod';
export const UserResponseSchema = z.object({
  id: z.string().uuid(),
  email: z.string().email(),
  name: z.string(),
  role: z.enum(['admin', 'member', 'viewer']),
  createdAt: z.string().datetime(),
});
export type UserResponse = z.infer<typeof UserResponseSchema>;

Test Patterns

CRUD Lifecycle Test

import { test, expect } from '../fixtures/api.fixture';

test.describe.serial('Projects CRUD lifecycle', () => {
  let projectId: string;

  test('CREATE', async ({ authedApi }) => {
    const res = await authedApi.post('/api/projects', {
      data: { name: 'Lifecycle Project', description: 'CRUD test' },
    });
    expect(res.status()).toBe(201);
    const body = await res.json();
    expect(body).toHaveProperty('id');
    projectId = body.id;
  });

  test('READ', async ({ authedApi }) => {
    const res = await authedApi.get(`/api/projects/${projectId}`);
    expect(res.status()).toBe(200);
    expect((await res.json()).name).toBe('Lifecycle Project');
  });

  test('UPDATE', async ({ authedApi }) => {
    const res = await authedApi.patch(`/api/projects/${projectId}`, {
      data: { name: 'Updated Name' },
    });
    expect(res.status()).toBe(200);
    expect((await res.json()).name).toBe('Updated Name');
  });

  test('DELETE', async ({ authedApi }) => {
    expect((await authedApi.delete(`/api/projects/${projectId}`)).status()).toBe(204);
  });

  test('VERIFY DELETED', async ({ authedApi }) => {
    expect((await authedApi.get(`/api/projects/${projectId}`)).status()).toBe(404);
  });
});

Auth Flow Testing

test.describe('Authentication flows', () => {
  test('successful login returns tokens', async ({ request }) => {
    const res = await request.post('/api/auth/login', {
      data: { email: 'user@example.com', password: 'correct-password' },
    });
    expect(res.status()).toBe(200);
    const body = await res.json();
    expect(body).toHaveProperty('accessToken');
    expect(body).toHaveProperty('refreshToken');
  });

  test('invalid credentials return 401', async ({ request }) => {
    const res = await request.post('/api/auth/login', {
      data: { email: 'user@example.com', password: 'wrong' },
    });
    expect(res.status()).toBe(401);
  });

  test('expired token returns 401', async ({ request }) => {
    const res = await request.get('/api/users/me', {
      headers: { Authorization: 'Bearer expired-token-here' },
    });
    expect(res.status()).toBe(401);
  });

  test('token refresh provides new access token', async ({ request }) => {
    const { refreshToken } = await (await request.post('/api/auth/login', {
      data: { email: 'user@example.com', password: 'correct-password' },
    })).json();
    const refreshRes = await request.post('/api/auth/refresh', { data: { refreshToken } });
    expect(refreshRes.status()).toBe(200);
    expect((await refreshRes.json())).toHaveProperty('accessToken');
  });

  test('insufficient permissions return 403', async ({ request }) => {
    const { accessToken } = await (await request.post('/api/auth/login', {
      data: { email: 'viewer@example.com', password: 'viewer-password' },
    })).json();
    const res = await request.delete('/api/admin/users/some-id', {
      headers: { Authorization: `Bearer ${accessToken}` },
    });
    expect(res.status()).toBe(403);
  });
});

Error Response Validation

test.describe('Error responses', () => {
  test('400 - malformed request body', async ({ request }) => {
    const res = await request.post('/api/projects', { data: { name: '' } });
    expect(res.status()).toBe(400);
    const body = await res.json();
    expect(body.details).toEqual(
      expect.arrayContaining([expect.objectContaining({ field: 'name' })]),
    );
  });

  test('422 - validation error with field details', async ({ request }) => {
    const res = await request.post('/api/users', { data: { email: 'not-an-email', name: 'Test' } });
    expect(res.status()).toBe(422);
    expect((await res.json()).details).toEqual(
      expect.arrayContaining([expect.objectContaining({ field: 'email' })]),
    );
  });

  test('429 - rate limiting returns retry-after header', async ({ request }) => {
    const responses = await Promise.all(
      Array.from({ length: 20 }, () => request.get('/api/status')),
    );
    const rateLimited = responses.find(r => r.status() === 429);
    if (rateLimited) {
      expect(rateLimited.headers()['retry-after']).toBeDefined();
    }
  });
});

Pagination Testing

test('first page returns correct metadata', async ({ request }) => {
  const body = await (await request.get('/api/projects?page=1&pageSize=10')).json();
  expect(body.page).toBe(1);
  expect(body.items.length).toBeLessThanOrEqual(10);
  expect(body.total).toBeGreaterThanOrEqual(body.items.length);
});

test('out of bounds page returns empty items', async ({ request }) => {
  const body = await (await request.get('/api/projects?page=99999&pageSize=10')).json();
  expect(body.items).toHaveLength(0);
});

test('invalid page size is rejected', async ({ request }) => {
  expect((await request.get('/api/projects?page=1&pageSize=0')).status()).toBe(400);
});

File Upload/Download via API

test('upload via multipart form', async ({ request }) => {
  const res = await request.post('/api/files/upload', {
    multipart: {
      file: { name: 'sample.csv', mimeType: 'text/csv', buffer: Buffer.from('id,name\n1,Test') },
    },
  });
  expect(res.status()).toBe(201);
  expect((await res.json()).fileName).toBe('sample.csv');
});

test('download and verify headers', async ({ request }) => {
  const res = await request.get('/api/files/some-file-id/download');
  expect(res.headers()['content-disposition']).toContain('attachment');
});

GraphQL Testing

test.describe('GraphQL API', () => {
  const gql = (request: any, query: string, variables?: Record<string, unknown>) =>
    request.post('/graphql', { data: { query, variables } });

  test('query - fetches user by ID', async ({ request }) => {
    const body = await (await gql(request, `
      query GetUser($id: ID!) { user(id: $id) { id email name } }
    `, { id: 'user-1' })).json();

    expect(body.errors).toBeUndefined();
    expect(body.data.user).toMatchObject({ id: 'user-1', email: expect.any(String) });
  });

  test('mutation - creates a project', async ({ request }) => {
    const body = await (await gql(request, `
      mutation CreateProject($input: CreateProjectInput!) {
        createProject(input: $input) { id name }
      }
    `, { input: { name: 'GQL Project' } })).json();

    expect(body.errors).toBeUndefined();
    expect(body.data.createProject.name).toBe('GQL Project');
  });

  test('invalid query returns errors array', async ({ request }) => {
    const body = await (await gql(request, `query { nonExistentField }`)).json();
    expect(body.errors).toBeDefined();
    expect(body.errors[0]).toHaveProperty('message');
  });

});

Webhook Testing

import http from 'http';

test.describe('Webhook delivery', () => {
  let server: http.Server;
  let payloads: any[] = [];
  let webhookUrl: string;

  test.beforeAll(async () => {
    server = http.createServer((req, res) => {
      let body = '';
      req.on('data', (c) => (body += c));
      req.on('end', () => { payloads.push(JSON.parse(body)); res.writeHead(200).end(); });
    });
    await new Promise<void>((r) => server.listen(0, r));
    webhookUrl = `http://localhost:${(server.address() as any).port}`;
  });
  test.afterAll(() => server?.close());

  test('receives event on project creation', async ({ request }) => {
    const { id: hookId } = await (await request.post('/api/webhooks', {
      data: { url: webhookUrl, events: ['project.created'] },
    })).json();
    await request.post('/api/projects', { data: { name: 'Webhook Project' } });
    await new Promise((r) => setTimeout(r, 2000));
    expect(payloads.at(-1).event).toBe('project.created');
    await request.delete(`/api/webhooks/${hookId}`);
  });
});

Performance Assertions

test('GET /api/users responds within 500ms', async ({ request }) => {
  const start = Date.now();
  const res = await request.get('/api/users');
  expect(res.ok()).toBeTruthy();
  expect(Date.now() - start).toBeLessThan(500);
});

test('response payload stays under 1MB', async ({ request }) => {
  const body = await (await request.get('/api/users')).body();
  expect(body.length / 1024).toBeLessThan(1024);
});

test('handles 50 concurrent requests without errors', async ({ request }) => {
  const results = await Promise.all(
    Array.from({ length: 50 }, () => request.get('/api/status').then(r => r.status())),
  );
  expect(results.every(s => s >= 200 && s < 500)).toBe(true);
});

Anti-Patterns

1. Hardcoded auth tokens

Tokens expire, rotate, and differ across environments. Use a login fixture that acquires tokens dynamically.

2. Testing against production

API tests create, modify, and delete data. Run against a dedicated test environment or local instance.

3. Not validating error responses

Happy-path-only suites miss the most common production issues. Test 400, 401, 403, 404, and 500 responses for every endpoint.

4. Ignoring response headers

Headers carry cache directives, rate limit info, content type, and CORS policy. If your API sets them, assert on them.

5. No cleanup after test data creation

Tests that create resources without deleting them pollute the database. Use afterEach/afterAll hooks or fixture teardown.

6. Treating API tests as unit tests

Don't mock the database -- API tests verify the contract from the consumer's perspective.

7. Ignoring idempotency

PUT and DELETE should be idempotent. Test that calling them twice produces the same result.


Done When

  • Every target endpoint has at least a happy-path test and at least one error-path test (4xx or 5xx response validated)
  • Auth flow tested as its own describe block: successful login, invalid credentials, expired token, and permission boundary (403)
  • Schema validation assertions on response shape using Zod or AJV — not just toHaveProperty spot-checks
  • Contract tests in place for any endpoint consumed by a different team or service (shared schema file or Pact)
  • Test suite runs cleanly in CI without any external service dependencies — all third-party calls mocked or virtualized

Related Skills

  • playwright-automation -- Browser-based E2E testing, Page Object Model, and combined browser + API patterns.
  • ci-cd-integration -- Running API test suites in CI pipelines, parallelization, and environment management.
  • test-strategy -- Deciding what to test at the API layer vs. unit vs. E2E.
  • self-healing-tests -- Reducing maintenance burden when API contracts evolve.

适合场景

01

用户想查找某类 Agent Skill 时

02

需要根据任务场景推荐可安装能力包时

03

需要对比不同来源的安装命令和来源信息时

能力概览

能力 1

按任务关键词查找相关 Skills

能力 2

展示可复制的安装命令

能力 3

保留来源站点、仓库和原始说明,方便继续核验

能力 4

展示第三方安全扫描或审计结果

安装后应在对应宿主中按原始 README 的触发条件使用;具体调用方式请以来源页面和 README 为准。

平台分布

Codex

34.98%
按下载量换算39

Claude

29.43%
按下载量换算33

Cursor

17.93%
按下载量换算20

Gemini CLI

9.5%
按下载量换算11

安全审计

Gen Agent Trust Hub

通过

Socket

通过

Snyk

通过

权限和风险

敏感数据

该 Skill 可能接触密钥、Token、环境变量或敏感配置,应进入高风险复核队列,默认不自动发布。

安装前确认

本站仅展示第三方公开信息,不托管安装包,不提供自动安装或运行环境。安装前应自行审查源码、依赖和命令行为。当前只有一个来源,正式发布前建议补源仓库或其他目录站核验。

来源信息

继续浏览同类 Skills