Generate API Test Cases from an OpenAPI Spec

Turn OpenAPI constraints into a test matrix: valid, invalid and boundary cases with expected status codes, a worked example, and a runnable Node script.

On this page

To generate test cases from an OpenAPI spec, read each operation's request schema and turn every constraint into at least one passing and one failing case. A required field becomes a test that omits it. A type becomes a test that sends the wrong type. minLength and maxLength become boundary tests just inside and just outside the limits. An enum becomes one test per allowed value plus one with a value outside the list. Then take the expected status for each case from the operation's documented responses: usually 201 or 200 for valid input and 400 or 422 for invalid input. The rest of this guide walks through that process on a real spec.

What each schema keyword gives you

JSON Schema keywords in the request body map directly to test ideas. The table lists the ones you will meet most often. Keep the right-hand column in mind: a failing case is only useful if you know which status the API should return.

KeywordValid caseInvalid caseExpected status for invalid
requiredAll required fields presentOmit each required field, one per test400 or 422
typeValue of the declared typeString where a number is expected, object where a string is expected400 or 422
minLength / maxLengthExactly min, exactly maxmin - 1, max + 1400 or 422
minimum / maximumExactly the boundsOne step below and above400 or 422
enumEach allowed valueA value not in the list, and a case variant like "Pro"400 or 422
patternA value that matchesWrong case, wrong length, extra characters400 or 422
formatA well-formed email, date or uuidA malformed value400 or 422, if the server validates format

The last row has a caveat. In JSON Schema, format is an annotation by default, so a server that validates requests with a schema library may accept "not-an-email" for format: email unless format checking is switched on. Write the test anyway. If it fails, you have found either a validation gap or a spec that promises more than the server enforces.

The example spec

Here is a signup endpoint with the kinds of constraints real APIs use. APIForge scored this spec 100 out of 100 for design: it has a versioned path, a summary and description, and documents 201, 400 and 409. A perfect design score only tells you the contract is well written. It says nothing about whether the server honors it, which is what the tests are for.

YAMLopenapi.yaml
openapi: 3.0.3
info:
  title: Signups API
  version: 1.0.0
paths:
  /v1/signups:
    post:
      summary: Create a signup
      description: Registers a new account for an email address.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, password, plan]
              properties:
                email:
                  type: string
                  format: email
                  maxLength: 254
                password:
                  type: string
                  minLength: 12
                  maxLength: 128
                plan:
                  type: string
                  enum: [free, pro]
                referralCode:
                  type: string
                  pattern: "^[A-Z0-9]{8}$"
      responses:
        "201":
          description: Signup created
        "400":
          description: Validation failed
        "409":
          description: Email already registered

The test matrix for POST /v1/signups

We derived these thirteen cases by hand from the constraints above. Each changes one thing from a known-good baseline, so a failure points at a single rule. The baseline body is {"email": "dana@example.com", "password": "correct-horse-9", "plan": "free"}.

IDTypeChange from baselineExpected
T01validNone201
T02validplan: "pro" and referralCode: "AB12CD34"201
T03invalidRemove email400
T04invalidRemove password400
T05invalidRemove plan400
T06invalidplan: "enterprise"400
T07edgepassword with 11 characters400
T08edgepassword with exactly 12 characters201
T09edgepassword with exactly 128 characters201
T10edgepassword with 129 characters400
T11invalidreferralCode: "ab12cd34" (lowercase)400
T12invalidemail: "not-an-email"400 if format is enforced
T13invalidSend T01 twice with the same email201, then 409
Thirteen cases from one operation. Use a unique email per run for the valid cases, or T13's duplicate rule will fail them.

Why boundaries get their own tests

Most length bugs are off-by-one errors. A developer writes password.length > 12 instead of >= 12, and a 12-character password the spec allows gets rejected. Testing only a comfortable value like 20 characters never finds that. Testing 11, 12, 128 and 129 does, because those four values sit on either side of each limit. The same applies to numeric minimum and maximum, to maxItems on arrays, and to maxLength on strings that end up in a database column with its own size limit. If the column is shorter than the spec's maxLength, T09 is the test that exposes it.

Cases the spec cannot give you

Schema-driven generation covers input validation well, but authentication, state, duplicates, rate limits and error formats live outside the request schema. Add these by reading the rest of the operation and your own business rules.

  • Authentication and authorization. If the operation declares security, test a missing token (401), an expired token, and a valid token without permission (403). The OpenAPI authentication example shows how these are declared.
  • State. T13 needs an existing account. Tests for update and delete need a resource that exists and one that does not (404).
  • Duplicates and retries. Decide what a repeated request should do and test it; the idempotency glossary entry explains the options.
  • Rate limits. Send requests until you receive 429 and check that a Retry-After header comes back, as described under rate limiting.
  • Error body shape. A 400 with the right status but an unstructured body still breaks clients. The REST API error handling guide covers a consistent error format.

Check more than the status code

A test that only compares status codes passes when the server returns 201 with the wrong body. For every case, also validate the response body against the schema documented for the status you received. Validating API responses against your OpenAPI schema explains how, including what to do about extra fields and undocumented status codes. For T01, also check that the response does not echo the password back, a mistake schema validation catches only if the response schema forbids additional properties.

Run the matrix in CI with Node

Once the cases exist as data, running them is a short loop. The script below uses Node's built-in test runner and fetch, so it needs no extra dependencies. Keep the cases in a JSON file next to the spec and review changes to both in the same pull request.

JavaScriptsignups.test.mjs
import { test } from "node:test";
import assert from "node:assert/strict";

const BASE = process.env.API_BASE_URL;
const email = () => `qa+${Date.now()}${Math.random().toString(36).slice(2, 6)}@example.com`;
const baseline = () => ({ email: email(), password: "correct-horse-9", plan: "free" });

const cases = [
  { id: "T01", body: baseline(), expected: 201 },
  { id: "T03", body: (({ email, ...rest }) => rest)(baseline()), expected: 400 },
  { id: "T06", body: { ...baseline(), plan: "enterprise" }, expected: 400 },
  { id: "T07", body: { ...baseline(), password: "a".repeat(11) }, expected: 400 },
  { id: "T08", body: { ...baseline(), password: "a".repeat(12) }, expected: 201 },
  { id: "T10", body: { ...baseline(), password: "a".repeat(129) }, expected: 400 }
];

for (const c of cases) {
  test(`${c.id} returns ${c.expected}`, async () => {
    const res = await fetch(`${BASE}/v1/signups`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(c.body)
    });
    assert.equal(res.status, c.expected);
  });
}
Run with: API_BASE_URL=https://staging.example.com node --test. Each valid case gets a fresh email so T13's duplicate rule does not interfere.

Generating the cases automatically

Writing the matrix by hand is fine for one endpoint and slow for fifty. Deterministic generators such as Tcases for OpenAPI read the spec and produce cases that cover both valid inputs and inputs that should be rejected, which suits teams that want reproducible output.

In APIForge, open the API testing workbench, load your spec, and generate tests for an endpoint, a tag, or the whole API. The AI writes 8 to 10 cases per endpoint, each labeled valid, invalid or edge, with a payload, an expected status and the reason for the case. You can run each case against your server from the browser or through the APIForge proxy, then validate the response body against the schema. A valid case passes on the exact expected status, or on any 2xx when a 2xx was expected; invalid and edge cases must match exactly. Export the cases as cURL, JavaScript fetch, JSON or a Postman collection. Treat AI output as a draft: check each expected status against the responses your spec documents before you commit the cases to CI.

Before generating tests, score the spec with the API quality checker. Missing error responses and untyped fields produce weak tests, because a generator cannot invent constraints the spec never states. The API testing best practices guide covers how these tests fit alongside integration and load testing.

Frequently asked questions

Both are common. RFC 9110 defines 400 for requests the server cannot process because of a client error, and 422 for well-formed content the server cannot process. Pick one, document it in the spec, and make your tests expect exactly that code.

Start with one valid case, one case per required field, one per type, and two to four per length, range or enum constraint. The signup example above needs thirteen. Add cases for auth, state and duplicates where the operation needs them.

Review them first. A model can guess an expected status the spec does not document, or produce a payload that tests two rules at once. Check each case against the spec's responses, then commit it.

Sources

  1. RFC 9110: HTTP Semantics, status codesIETF
  2. OpenAPI Specification 3.1.0OpenAPI Initiative
  3. Tcases for OpenAPIGitHub
  4. JSON Schema type reference (format as annotation)json-schema.org