Skip to content

Negative testing and schema drift

Assertions check your API does the right thing with good input. These two checks cover what happens with bad input, and what happens when a response quietly changes shape.

Where
A request's Negative Testing and Schema Check tabs
You need
A request that already works
Write
Nothing — cases are generated

Negative testing

A green suite proves your API works when used correctly. It says nothing about what happens when it is used incorrectly — and an API that accepts a negative price, a missing required field or an unauthenticated request is broken in a way no amount of happy-path testing will reveal.

QAClan takes a request that works, generates deliberately wrong versions of it, sends each one and grades the answer. The question is not did it work? but did it refuse properly?

  1. Open a request and go to Negative Testing.

  2. Choose Generate tests.

    Cases appear grouped by category, each one a checkbox.

  3. Untick anything that does not apply to your API.

  4. Choose Run Negative Testing.

The Negative Testing tab: the Inherit, On and Off setting, Generate tests and Run Negative Testing buttons, and the generated case matrix grouped by category.The Negative Testing tab: the Inherit, On and Off setting, Generate tests and Run Negative Testing buttons, and the generated case matrix grouped by category.
The generated cases, grouped by category. Each checkbox is one case you can turn off.

What gets generated

Three categories, each attacking a different part of the request.

input-validation — the values you sent

One set of these per field in the body.

CaseSendsExpects
missingThe field removed entirely4xx
wrong typeA number where a string belongs, and the reverse4xx
nullThe field set to null4xx
emptyAn empty string, or an empty array4xx
not in enumA value outside the allowed set4xx
below minimum · above maximumOne under the lower bound, one over the upper4xx
too short · too longStrings either side of the length limits4xx
bad formatText that is not the declared format — not an email, not a date4xx

The last four only appear when the API declares bounds, an enum, lengths or a format — which an OpenAPI import does and a captured request cannot.

Two more that are generated but switched off
CaseSendsOff because
unexpected extra fieldAn extra key the API never asked forPlenty of APIs legitimately ignore extras
oversized payloadA 100,000-character stringIt can stress a real backend

Tick either one if it is a rule your API is meant to enforce.

request-level — the shape of the request

CaseSendsExpects
No auth tokenThe same request with authentication dropped401
Garbage auth tokenBearer qaclan-invalid-token401
Wrong methodGET against an endpoint that expects a write405
Wrong Content-TypeA JSON body labelled text/plain415
Unknown routeA path segment that does not exist, appended to the URL404
Malformed JSON bodyA truncated body — {"qaclan": 400

The two auth cases only appear when the request actually has auth, and Wrong Content-Type only when it has a body. For a GET, the wrong-method case would be DELETE — generated, but switched off, because a DELETE that succeeds destroys something.

injection — what your API does with hostile input

Four payloads, placed into every string field and every string query parameter.

PayloadSendsLooking for
SQL injection' OR '1'='1A query built by string concatenation
Cross-site scripting<script>alert(1)</script>Markup echoed back unescaped
Path traversal../../../../etc/passwdA file path assembled from user input
Null byteA string containing \0Parsers that truncate at the byte, or crash on it

These do not expect a particular status. Neutralising the payload and returning 200 is as correct as refusing with a 400. What counts as a failure is a 5xx or the payload echoed back in the response.

An OpenAPI import makes all of this sharper

A described API declares which fields are required, what types they are and what bounds they have, so the generated cases are precise instead of conservative. Without a description, every field gets a missing case on the assumption it might be required — prune what does not apply.

Prefer the terminal?
qaclan api negatives <name_or_id>
qaclan api negatives <name_or_id> --collection Orders --env staging

Reading the result

The headline is false passes: cases where your API accepted something it should have rejected. That is the finding that matters; the rest is detail.

Negative testing results: a red banner reading 15 false passes, API accepted invalid input, with counts for rejected, false pass, server error and failed, above a matrix showing 201 returned for every invalid input.Negative testing results: a red banner reading 15 false passes, API accepted invalid input, with counts for rejected, false pass, server error and failed, above a matrix showing 201 returned for every invalid input.
A real result against a deliberately permissive API: every invalid body came back 201, so 15 cases are false passes.
OutcomeSeverityMeans
Invalid input acceptedCriticalA false pass. Fix this first.
Server crashed (5xx)MajorIt refused, but by falling over
Rejected with a different 4xxMinorRight behaviour, unexpected code
Rejected as expectedNonePassed

A run rolls up to the worst severity it saw, so one critical finding among two hundred passing cases is not buried.

Why a wrong 4xx is only Minor, and how injection is graded differently

By default a case expects any 4xx, not one specific code. Whether your API answers 400 or 422 for a bad field is a matter of taste, and grading that as a failure would bury the findings that matter under noise. If your API has a contract about exact codes, make the case strict in the app and only that code passes.

For injection, the status code is not the point at all. Only two things fail: the payload appearing in the response, and a 5xx. Both are graded Critical; anything else passes.

A case whose request never completed — a timeout or a transport error — is recorded as Minor and treated as inconclusive. It is never counted as a false pass and never forces the run to fail.

Cases that change data

Some generated cases use POST, PUT, PATCH or DELETE. Firing those writes to whatever the request points at, so they do not run until you confirm.

A confirmation dialog reading: Run destructive negative tests? This fires 25 negative cases using state-changing methods (POST) against environment (no environment). Invalid and injection payloads may be written to that target.A confirmation dialog reading: Run destructive negative tests? This fires 25 negative cases using state-changing methods (POST) against environment (no environment). Invalid and injection payloads may be written to that target.
The gate names the case count, the methods and the environment being targeted.

Point this at a test environment

Negative testing sends malformed and hostile requests on purpose. Run it against staging or a dedicated test environment, never against production data. From the terminal nothing fires without --yes.

Turning it on and off, and why it never runs inside a suite

A collection carries a default for everything inside it; a request is inherit, on or off. That lets you enable negative testing across a collection and exempt the handful of requests where it makes no sense.

A suite exists to walk a working journey in order. Deliberately breaking a request in the middle would derail everything after it, so negative cases are excluded from suite runs by design — and an ordinary Sendnever fires them either. Run them from the request's Negative Testing tab.

Generated cases are saved against the request rather than produced fresh each run, so switching one off sticks. Regenerating shows what was added and removed compared to the set you had, instead of silently replacing it.

Schema drift — when the response changes shape

An API can keep returning 200 while quietly changing what it returns. A field is renamed, a number becomes a string, something always present starts coming back null. Assertions catch that only if you happened to assert on that exact field.

Drift compares the shape of each response against a shape frozen earlier, and reports what changed whether or not you were checking it.

The Schema Diff tab reading: Schema changed, 2 breaking, 3 added. Under BREAKING, userId and title are marked removed; under ADDED, postId, name and email are listed.The Schema Diff tab reading: Schema changed, 2 breaking, 3 added. Under BREAKING, userId and title are marked removed; under ADDED, postId, name and email are listed.
Two fields the response stopped returning are breaking; three it started returning are additive.

The baseline freezes itself

You do not create one. The first time a request comes back with a JSON body and a non-error status, that shape is stored. Later responses never overwrite it.

When a change is intentional — you shipped the new field on purpose — Update response schema re-freezes the baseline to the current shape, and drift is measured from there.

Breaking and additive

ChangeClassified asWhy
A field was removedBreakingAnything reading that field now gets nothing
A field changed typeBreakingA consumer expecting a number and receiving a string fails, often far from the cause
A field became nullableBreakingCode that never had to handle null now does
An array's element type changedBreakingSame problem as a type change, one level down
A field was addedAdditiveExisting consumers are unaffected, but you should know

Each difference names the path to the field, what the baseline expected there, and what actually arrived.

Why a response you know changed can come back clean

Some parts of a response cannot be described confidently — an empty array says nothing about what it would contain, and nesting deeper than four levels is capped. Both are recorded as unknown.

Wherever either side of a comparison is unknown, no difference is reported. That is deliberate: a guess would produce false alarms. If a change you know about is reported as clean, an unknown on one side is the likely reason — send the request once more with a populated array and re-freeze.

The baseline is captured on the first send whether or not drift checking is enabled, so the Schema tab and the extractor picker have something to show straight away.

Where the verdicts show up

  • On the request — the Schema Diff and Negative Testing result tabs.
  • In run results, rolled up to the worst severity seen.
  • In the cloud, if you are signed in — verdicts sync with the run, so a teammate sees them without your machine. See The cloud workspace.

Where to go next

ToRead
Build the requests these checks run againstAPI testing
Write a check an assertion cannot expressScript API reference
Every flag on qaclan api negativesCLI reference