api-test-suite
CoreGenerate a runnable API test suite — contract tests, integration tests, and CI config from an OpenAPI spec or existing API.
What it does
The agent reads your API's source of truth (a spec file or the route code itself) and writes a test package you can run locally and wire into CI. It covers the happy path, the obvious error cases (auth, validation, not-found), and the edge cases people forget (empty bodies, malformed input, rate limits). Then it runs the suite and tells you what passed, what failed, and whether the failure is a bug in your API or a wrong expectation in the test. This is a generator, not a hosted runner. The output is real files in your repo that you own and can edit.
How an agent uses it
- A new API project needs test coverage before it ships.
- The user says "write tests for these endpoints", "add contract tests", or "test my API".
- Pre-release or pre-merge verification of an HTTP API.
- You have an OpenAPI/Postman artifact and want it turned into executable tests.
- You are onboarding to an API codebase and want a safety net first.
Do not use it for browser UI flows (use a browser-test skill) or for pure unit tests of non-API logic (those belong in a unit test file, not the API suite).
What you get
Install this skill and your Hermes agent can generate a runnable api test suite — contract tests, integration tests, and ci config from an openapi spec or existing api. No manual setup, no scripts to run — the agent handles it.
Install command
hermes skills install https://raw.githubusercontent.com/THEROCKSSS/hermes-skills-portfolio/main/skills/api-test-suite/SKILL.md
---
name: api-test-suite
description: Use when a new or existing API needs test coverage before a release or merge, when onboarding to an unfamiliar API codebase and wanting a safety net first, or when an OpenAPI spec/Postman collection exists and should be turned into a runnable pytest/vitest suite with contract and integration tests.
version: 1.0.0
author: Hermes Agent
license: MIT
metadata:
hermes:
tags: [api-testing, contract-tests, integration-tests, pytest, vitest, ci-cd]
related_skills: [openapi-generator, http-api-tester, github-actions-ci]
---
# api-test-suite
## Overview
Generate a runnable API test package from an OpenAPI spec, a Postman collection, or by scanning an existing API codebase. The agent produces real, locally-runnable test files — not a hosted-run stub — covering happy path, error cases, and edge cases, then runs the suite and reports pass/fail.
## When to Use
- A new API project needs test coverage before it ships.
- The user says "write tests for these endpoints", "add contract tests", or "test my API".
- Pre-release or pre-merge verification of an HTTP API.
- You have an OpenAPI/Postman artifact and want it turned into executable tests.
- You are onboarding to an API codebase and want a safety net first.
Do not use it for browser UI flows (use a browser-test skill) or for pure unit tests of non-API logic (those belong in a unit test file, not the API suite).
## Workflow
1. **Pick the input.** One of:
- An OpenAPI/Swagger spec (`openapi.yaml`, `swagger.json`).
- A Postman collection (`collection.json`).
- An API codebase (FastAPI/Express/Flask/Nest/etc.) — scan route definitions.
2. **Enumerate endpoints** with method, path, auth scheme, request shape, and response codes.
3. **Derive test cases per endpoint:**
- Happy path — 2xx, response shape matches spec.
- Auth missing/invalid — 401/403.
- Validation failure — 400, with the documented error shape.
- Not found — 404 for unknown IDs.
- Edge cases — empty body, oversized input, malformed JSON, pagination bounds, rate limiting.
4. **Choose a runner by stack:**
- Node/TS → `vitest` + `supertest` (in-process) or `axios` (live server).
- Python → `pytest` + `httpx` (live server) or `fastapi.testclient` (in-process).
5. **Emit the file tree** (see Test Runner Setup).
6. **Run the suite.** Report pass/fail per file. If a test fails, decide whether it is a real bug in the API or a wrong expectation in the test — fix the API when the expectation is correct, fix the test when it is not. Never delete or weaken an assertion just to turn it green.
## Contract Tests
Contract tests assert that the live API's responses conform to the published schema. They catch drift between spec and implementation.
**From an OpenAPI spec — Python with schemathesis (property-based, hits the running server):**
```python
# tests/contract/test_contract.py
import schemathesis
schema = schemathesis.from_uri("http://localhost:8000/openapi.json")
@schema.parametrize()
def test_api_contract(case, base_url):
# `case` is generated from the spec; this verifies the server honors it
response = case.call(base_url)
case.validate_response(response)
```
If schemathesis is too heavy, validate responses against a JSON Schema extracted from the spec:
```python
# tests/contract/test_shapes.py
import json
import httpx
from jsonschema import Draft202012Validator
spec = json.load(open("openapi.json"))
# pull the response schema for GET /markets -> 200
schema = spec["paths"]["/markets"]["get"]["responses"]["200"]["content"][
"application/json"
]["schema"]
def test_markets_shape(base_url):
r = httpx.get(f"{base_url}/markets?limit=10")
assert r.status_code == 200
Draft202012Validator(schema).validate(r.json())
```
**From an OpenAPI spec — Node with a lightweight assertion:**
```ts
// tests/contract/markets.spec.ts
import { expect } from 'vitest'
import { client } from '../helpers/http'
it('GET /markets 200 matches spec shape', async () => {
const r = await client.get('/markets?limit=10')
expect(r.status).toBe(200)
expect(Array.isArray(r.data.data)).toBe(true)
expect(r.data).toHaveProperty('total')
})
```
Rule: contract tests must read the schema from the artifact, not hardcode a copy that can silently rot.
## Integration Tests
Integration tests exercise real HTTP behavior against a running server (or an in-process app) with auth, test data, and cleanup.
**Python — fixtures for auth, data, cleanup (pytest):**
```python
# tests/integration/test_orders.py
import pytest, httpx
@pytest.fixture
def auth_headers():
# mint a short-lived test token; never use a real user's credentials
token = mint_test_token(scopes=["orders:write"])
return {"Authorization": f"Bearer {token}"}
@pytest.fixture
def created_order(base_url, auth_headers):
r = httpx.post(f"{base_url}/orders", json={"item": "widget", "qty": 1},
headers=auth_headers)
assert r.status_code == 201
yield r.json()["id"]
# cleanup runs even if the test fails
httpx.delete(f"{base_url}/orders/{r.json()['id']}", headers=auth_headers)
def test_create_order_happy(base_url, auth_headers, created_order):
assert isinstance(created_order, str)
def test_create_order_requires_auth(base_url):
r = httpx.post(f"{base_url}/orders", json={"item": "widget"})
assert r.status_code == 401
def test_create_order_bad_payload(base_url, auth_headers):
r = httpx.post(f"{base_url}/orders", json={}, headers=auth_headers)
assert r.status_code == 400
```
**Node — vitest + supertest (in-process, no separate server):**
```ts
// tests/integration/orders.test.ts
import request from 'supertest'
import { app } from '../../src/app'
import { testToken } from '../helpers/auth'
describe('POST /orders', () => {
it('creates an order with valid auth', async () => {
const r = await request(app)
.post('/orders')
.set('Authorization', `Bearer ${testToken()}`)
.send({ item: 'widget', qty: 1 })
expect(r.status).toBe(201)
})
it('rejects missing auth', async () => {
const r = await request(app).post('/orders').send({ item: 'widget' })
expect(r.status).toBe(401)
})
it('rejects empty body', async () => {
const r = await request(app)
.post('/orders')
.set('Authorization', `Bearer ${testToken()}`)
.send({})
expect(r.status).toBe(400)
})
})
```
## Test Runner Setup
Emit this layout so the suite is reproducible:
```
tests/
conftest.py # pytest fixtures: base_url, auth_headers, data, cleanup
contract/
test_contract.py # schema-conformance checks
test_shapes.py
integration/
test_orders.py # real HTTP happy/error/edge cases
helpers/
http.ts / http.py # shared client, base URL from env
auth.ts / auth.py # test-token minting
openapi.json # spec under test (if available)
pytest.ini / vitest.config.ts
```
**Python — `pytest.ini`:**
```ini
[pytest]
testpaths = tests
addopts = -q --tb=short
env =
BASE_URL=http://localhost:8000
```
Install: `pip install pytest httpx schemathesis`
**Node — `vitest.config.ts`:**
```ts
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
globals: true,
environment: 'node',
include: ['tests/**/*.test.ts', 'tests/**/*.spec.ts'],
},
})
```
Install: `npm i -D vitest supertest`
## CI Integration
Run contract + integration tests against a service spun up in the pipeline. GitHub Actions / Forgejo Actions share the same YAML:
```yaml
name: api-tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
services:
api:
image: ghcr.io/your-org/your-api:ci
ports: ["8000:8000"]
options: >-
--health-cmd "curl -f http://localhost:8000/health"
--health-interval 10s --health-timeout 5s --health-retries 5
env:
BASE_URL: http://localhost:8000
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: "3.12" }
- run: pip install pytest httpx schemathesis
- run: pytest tests/ --junitxml=report.xml
- uses: actions/upload-artifact@v4
if: always()
with: { name: api-test-report, path: report.xml }
```
For Node, swap the setup step for `actions/setup-node` and the run step for `npx vitest run`.
## Common Pitfalls
1. **Mocking the database so tests lie.** Use a real test database or per-test transaction rollback. A test that mocks storage at the wrong boundary passes while the API is broken.
2. **Hardcoding the response shape instead of reading the spec.** Contract tests must derive expectations from the OpenAPI/Postman artifact, or they drift and become noise.
3. **Shared mutable state across tests.** Each test must create and clean up its own data. Seeds that depend on order will flake in CI.
4. **Using a real user's token for auth.** Mint short-lived test tokens scoped to the test tenant; rotate secrets from env, never commit them.
5. **Rate limiting breaking CI.** Hit the API serially in contract runs, or raise the limit for the test tenant. A 429 is a test-harness problem, not an API bug, until proven otherwise.
6. **Treating generated tests as a substitute for reading the spec.** The suite documents behavior; the agent should still confirm the API's intent with the user for ambiguous endpoints.
7. **Flaky network timing.** Add bounded retries only on connection errors, never on assertion failures.
## Verification Checklist
- [ ] The suite runs with a single command (`pytest tests/` or `npx vitest run`) and exits 0, or every failure is a confirmed real API bug
- [ ] Contract tests read the schema from the OpenAPI/Postman artifact at runtime, not a hardcoded copy
- [ ] Every endpoint has at least a happy-path test and one auth/validation-failure test
- [ ] Auth uses minted test tokens, never a real user's credentials
- [ ] Fixtures that create data also tear it down (no orphaned test records after a run)
- [ ] CI YAML (if emitted) targets the correct service image/port and uploads the test report artifact
# api-test-suite
Turn an API into a runnable test suite — contract tests, integration tests, and CI config — generated from an OpenAPI spec, a Postman collection, or an existing API codebase.
## What it does
The agent reads your API's source of truth (a spec file or the route code itself) and writes a test package you can run locally and wire into CI. It covers the happy path, the obvious error cases (auth, validation, not-found), and the edge cases people forget (empty bodies, malformed input, rate limits). Then it runs the suite and tells you what passed, what failed, and whether the failure is a bug in your API or a wrong expectation in the test.
This is a generator, not a hosted runner. The output is real files in your repo that you own and can edit.
## Install
```bash
hermes skills install https://raw.githubusercontent.com/THEROCKSSS/hermes-skills-portfolio/main/skills/api-test-suite/SKILL.md
```
## Inputs
The skill accepts any one of these as the source of truth:
| Input | Example | What it drives |
|---|---|---|
| OpenAPI / Swagger spec | `openapi.yaml`, `swagger.json` | Endpoint list, shapes, response codes |
| Postman collection | `collection.json` | Endpoint list, example requests |
| API codebase | FastAPI, Express, Flask, Nest | Route scans, auth schemes |
If you have a spec, that wins — it is the contract. If you only have code, the agent scans route definitions and infers auth and shapes, then asks you to confirm anything ambiguous.
## What you get
```
tests/
conftest.py # pytest fixtures: base_url, auth_headers, cleanup
contract/
test_contract.py # schema-conformance checks (spec-driven)
test_shapes.py
integration/
test_orders.py # real HTTP happy / error / edge cases
helpers/
http.py # shared client, base URL from env
auth.py # test-token minting
openapi.json # spec under test (if you provided one)
pytest.ini
```
Node/TS projects get the same shape with `vitest.config.ts` and `*.test.ts` / `*.spec.ts` files, plus a `helpers/http.ts` and `helpers/auth.ts`.
## Contract tests
Contract tests assert the live API's responses conform to the published schema. They catch drift between what you documented and what you shipped.
```python
# tests/contract/test_shapes.py
import json
import httpx
from jsonschema import Draft202012Validator
spec = json.load(open("openapi.json"))
schema = (
spec["paths"]["/markets"]["get"]["responses"]["200"]["content"]
["application/json"]["schema"]
)
def test_markets_shape(base_url):
r = httpx.get(f"{base_url}/markets?limit=10")
assert r.status_code == 200
Draft202012Validator(schema).validate(r.json())
```
The rule that makes this worth having: the expectation is read from the spec, not copied into the test. A hardcoded copy silently rots; a spec-derived check fails the moment the API and the contract disagree, which is the whole point.
For deeper coverage, `schemathesis` generates requests from the spec and validates every response against it automatically:
```python
import schemathesis
schema = schemathesis.from_uri("http://localhost:8000/openapi.json")
@schema.parametrize()
def test_api_contract(case, base_url):
response = case.call(base_url)
case.validate_response(response)
```
## Integration tests
Integration tests exercise real HTTP behavior against a running server (or an in-process app) with auth, test data, and cleanup. Fixtures create their own data and tear it down, so tests stay isolated.
```python
# tests/integration/test_orders.py
import pytest, httpx
@pytest.fixture
def auth_headers():
return {"Authorization": f"Bearer {mint_test_token(scopes=['orders:write'])}"}
@pytest.fixture
def created_order(base_url, auth_headers):
r = httpx.post(f"{base_url}/orders", json={"item": "widget", "qty": 1},
headers=auth_headers)
assert r.status_code == 201
yield r.json()["id"]
httpx.delete(f"{base_url}/orders/{r.json()['id']}", headers=auth_headers)
def test_create_order_happy(base_url, auth_headers, created_order):
assert isinstance(created_order, str)
def test_create_order_requires_auth(base_url):
r = httpx.post(f"{base_url}/orders", json={"item": "widget"})
assert r.status_code == 401
def test_create_order_empty_body(base_url, auth_headers):
r = httpx.post(f"{base_url}/orders", json={}, headers=auth_headers)
assert r.status_code == 400
```
Node/TS, in-process with `supertest`:
```ts
// tests/integration/orders.test.ts
import request from 'supertest'
import { app } from '../../src/app'
import { testToken } from '../helpers/auth'
describe('POST /orders', () => {
it('creates an order with valid auth', async () => {
const r = await request(app)
.post('/orders')
.set('Authorization', `Bearer ${testToken()}`)
.send({ item: 'widget', qty: 1 })
expect(r.status).toBe(201)
})
it('rejects missing auth', async () => {
const r = await request(app).post('/orders').send({ item: 'widget' })
expect(r.status).toBe(401)
})
})
```
## Test runner setup
**Python** — `pip install pytest httpx schemathesis`, then `pytest.ini`:
```ini
[pytest]
testpaths = tests
addopts = -q --tb=short
env =
BASE_URL=http://localhost:8000
```
**Node/TS** — `npm i -D vitest supertest`, then `vitest.config.ts`:
```ts
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: { globals: true, environment: 'node',
include: ['tests/**/*.test.ts', 'tests/**/*.spec.ts'] },
})
```
## CI integration
Run the suite against a service the pipeline spins up. This YAML works in both GitHub Actions and Forgejo Actions:
```yaml
name: api-tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
services:
api:
image: ghcr.io/your-org/your-api:ci
ports: ["8000:8000"]
options: >-
--health-cmd "curl -f http://localhost:8000/health"
--health-interval 10s --health-timeout 5s --health-retries 5
env:
BASE_URL: http://localhost:8000
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: "3.12" }
- run: pip install pytest httpx schemathesis
- run: pytest tests/ --junitxml=report.xml
- uses: actions/upload-artifact@v4
if: always()
with: { name: api-test-report, path: report.xml }
```
Swap the setup/run steps for `actions/setup-node` and `npx vitest run` on a Node project.
## Coverage matrix
The agent generates at least one case per endpoint from each row:
| Category | Case | Expected |
|---|---|---|
| Happy path | Valid request, valid auth | 2xx, shape matches spec |
| Auth | Missing / invalid token | 401 / 403 |
| Validation | Missing required field, bad type | 400, documented error shape |
| Not found | Unknown ID | 404 |
| Edge | Empty body, malformed JSON, oversized input | 400 / 413 |
| Edge | Pagination bounds (`limit=0`, `limit=1000`) | 200 or 422, sane clamping |
| Edge | Rate limit (burst) | 429 with retry header |
## Honest limitations
- The suite documents observed behavior. For ambiguous endpoints the agent confirms intent with you rather than guessing the "right" contract.
- Contract tests check shape and codes; they do not prove business logic correctness. Pair them with a few hand-written behavior tests for your critical paths.
- Generated fixtures use a test tenant and short-lived tokens. You must wire real secret injection (`BASE_URL`, token minting) for your environment — the skill scaffolds it, it does not know your auth system.
- Rate-limited APIs will 429 under load; the skill serializes contract runs and raises the limit for the test tenant instead of masking failures.
## Part of
[Hermes Skills Portfolio](https://github.com/THEROCKSSS/hermes-skills-portfolio) — empowering skills for the Hermes agent.