Negative and Boundary Value Testing for APIs with Real Examples
Any developer can make an API work with perfect input. Your job as a tester is to find what happens with everything else: the empty field, the age of 17, the price of minus one, the 5 MB name, the emoji, the duplicated key. This is where most API bugs and many security issues live. In this lesson you’ll learn a systematic method for negative and boundary testing, and run more than 40 real cases against a live validation endpoint.
min-1, min, max and max+1 for numbers, lengths, sizes, dates and list counts.Why Negative Testing Matters So Much
"quantity": -1, so the line total became negative and the order total dropped below zero. Unvalidated amount: a wallet top-up API trusted an amount field sent from the client. 500 on emoji: a delivery app crashed when a customer named their address “Home 🏠”. Each one is a missing negative test.- Users make mistakes, old app versions send old formats, partner systems send surprises, and attackers send everything.
- Correct rejection protects data integrity: a bad request must not half-save.
- Clear error messages reduce support tickets and help front-end developers show the right field error.
The Validation Endpoint We’ll Test
POST /validate/user on the practice API validates a user payload without saving it. Its rules:
| Field | Rule |
|---|---|
name | Required string, 2 to 50 characters (spaces-only rejected) |
email | Required, valid email format |
age | Required whole number, 18 to 100 (not a string) |
role | Optional: admin, editor or viewer (default viewer) |
phone | Optional: 10-digit Indian mobile number starting with 6 to 9 |
| Anything else | Rejected as “Unknown field” |
https://sqatools.in/wp-json/sqa-api/v1/validate/usercurl -i \
-X POST \
-H "Content-Type: application/json" \
-d '{"name":"A","email":"asha@","age":"30","role":"manager","salary":1}' \
"https://sqatools.in/wp-json/sqa-api/v1/validate/user"One request, five errors, all reported together with the field name. That’s the behaviour to expect (and to ask for) from any API.
Technique 1: Boundary Value Analysis
Developers commonly write < where they meant <=. Boundaries catch those off-by-one bugs.
| Field | min-1 | min | max | max+1 |
|---|---|---|---|---|
age (18-100) | 17 → 422 | 18 → 200 | 100 → 200 | 101 → 422 |
name length (2-50) | 1 char → 422 | 2 chars → 200 | 50 chars → 200 | 51 chars → 422 |
ttl on /objects (10-1800) | 9 → 422 | 10 → 201 | 1800 → 201 | 1801 → 422 |
per_page (1-50) | 0 → 400 | 1 → 200 | 50 → 200 | 51 → 400 |
expires_in on login (5-3600) | 4 → 422 | 5 → 200 | 3600 → 200 | 3601 → 422 |
| Request body size (10 KB) | – | – | under 10 KB → ok | over 10 KB → 413 |
Other boundaries to look for in real APIs: dates (today, past, far future, Feb 29, time zones), list sizes (0 items, 1, max items in a cart), money (0, 0.01, max transaction limit), file sizes and counts (5th vs 6th saved address).
Technique 2: Equivalence Partitioning by Data Type
For each field, test one value from every “class” of wrong input:
| Class | Example for age | Practice API result |
|---|---|---|
| Valid | 30 | 200 |
| Number as string | “30” | 422 |
| Decimal | 30.5 | 422 |
| Negative | -1 | 422 |
| Null | null | 422 |
| Missing key | (no age) | 422 |
| Huge number | 1e309 | 422 |
| Boolean / array / object | true, [30], {“v”:30} | Should be 422 |
Do the same for strings (empty, spaces only, very long, Unicode, emoji, HTML, SQL), enums (wrong value, wrong case “Admin”), and formats (email without domain, phone with country code, date in the wrong format).
Technique 3: Structural (Request-Level) Negative Tests
These break the request itself, not one field:
| Case | How | Expected |
|---|---|---|
| Malformed JSON | {"name": "Asha", | 400 invalid_json |
| Empty body | No body | 400 missing_body |
| Wrong top-level type | [{"name":"Asha"}] | 400 invalid_body |
| Wrong Content-Type | text/plain | 415 |
| Oversized body | Over 10 KB | 413 |
| Unknown field | "salary": 1 | 422 “Unknown field” |
| Duplicate keys | {"age":30, "age":17} | Consistent rule (practice API: last value wins → 422) |
| Wrong method | DELETE /products/101 | 405 + Allow |
| Wrong URL / version | /v9/users | 404, no stack trace |
| Unsupported Accept | Accept: text/csv | 406 |
https://sqatools.in/wp-json/sqa-api/v1/validate/usercurl -i \
-X POST \
-H "Content-Type: application/json" \
-d '[{"name":"Asha"}]' \
"https://sqatools.in/wp-json/sqa-api/v1/validate/user""amount" and the backend reads the last one, an attacker can get a request approved for Rs 1 and executed for Rs 10,000. Whatever the rule, all layers must agree. Ideally, duplicate keys are rejected.Technique 4: Cross-Field and Business Rule Violations
min_pricegreater thanmax_price: the practice API returns 400 “min_price cannot be greater than max_price”.- End date before start date in a booking.
- Coupon valid only above Rs 500 applied to a Rs 499 cart.
- Delivery pincode outside the service area.
- Body
iddifferent from the URLidin a PUT (practice API: 422). - Transfer amount above the account balance or the daily limit.
https://sqatools.in/wp-json/sqa-api/v1/products?min_price=5000&max_price=100curl -i \
"https://sqatools.in/wp-json/sqa-api/v1/products?min_price=5000&max_price=100"Technique 5: Special Characters, Unicode and Injection Strings
| Input | Why | Expected |
|---|---|---|
आशा वर्मा, Zoë, O'Brien | Real names | Accepted and stored exactly |
😀🏠 | 4-byte UTF-8 (breaks some databases) | Accepted or cleanly rejected; never 500 |
" " | Whitespace only | Treated as empty (practice API: 422) |
" Asha " | Leading/trailing spaces | Trimmed or rejected consistently |
' OR '1'='1 | SQL injection | Stored as text or rejected; no SQL errors |
<script>alert(1)</script> | Stored XSS | Stored as text, escaped on display |
\u0000, control characters | Null bytes | Rejected |
| 10,000-character string | Length limits, buffer issues | 422/413, not 500 |
What a Good Error Response Looks Like
{
"error": {
"status": 422,
"code": "validation_failed",
"message": "2 field(s) failed validation.",
"details": [
{ "field": "age", "message": "age is required: a whole number from 18 to 100 (not a string)." },
{ "field": "phone", "message": "phone must be a 10-digit Indian mobile number starting with 6-9." }
]
}
}
Check every negative response for:
- The specific status code (not a generic 400 for everything, never 200, never 500).
- A machine-readable code and the field name, so apps can highlight the right input.
- All errors at once, not one per attempt.
- A human-readable message without internals (no SQL, stack traces or class names).
- The same error format on every endpoint.
- No side effects: nothing created, updated, charged or emailed.
Automating 40+ Negative Cases with Playwright
import { test, expect } from '@playwright/test';
const URL = 'https://sqatools.in/wp-json/sqa-api/v1/validate/user';
const valid = { name: 'Asha', email: 'asha@example.com', age: 30 };
type Case = { title: string; body: Record<string, unknown>; status: number; field?: string };
const cases: Case[] = [
// Boundaries
{ title: 'age 17', body: { ...valid, age: 17 }, status: 422, field: 'age' },
{ title: 'age 18', body: { ...valid, age: 18 }, status: 200 },
{ title: 'age 100', body: { ...valid, age: 100 }, status: 200 },
{ title: 'age 101', body: { ...valid, age: 101 }, status: 422, field: 'age' },
{ title: 'name 1 char', body: { ...valid, name: 'A' }, status: 422, field: 'name' },
{ title: 'name 2 chars', body: { ...valid, name: 'As' }, status: 200 },
{ title: 'name 50 chars', body: { ...valid, name: 'a'.repeat(50) }, status: 200 },
{ title: 'name 51 chars', body: { ...valid, name: 'a'.repeat(51) }, status: 422, field: 'name' },
// Types
{ title: 'age as string', body: { ...valid, age: '30' }, status: 422, field: 'age' },
{ title: 'age decimal', body: { ...valid, age: 30.5 }, status: 422, field: 'age' },
{ title: 'age null', body: { ...valid, age: null }, status: 422, field: 'age' },
{ title: 'name spaces', body: { ...valid, name: ' ' }, status: 422, field: 'name' },
// Formats and enums
{ title: 'email no domain', body: { ...valid, email: 'asha@' }, status: 422, field: 'email' },
{ title: 'unknown role', body: { ...valid, role: 'manager' }, status: 422, field: 'role' },
{ title: 'phone with +91', body: { ...valid, phone: '+919876543210' }, status: 422, field: 'phone' },
{ title: 'valid phone', body: { ...valid, phone: '9876543210' }, status: 200 },
{ title: 'unknown field', body: { ...valid, salary: 1 }, status: 422, field: 'salary' },
{ title: 'unicode name', body: { ...valid, name: 'आशा वर्मा' }, status: 200 },
];
for (const c of cases) {
test(`validate/user: ${c.title} -> ${c.status}`, async ({ request }) => {
const res = await request.post(URL, { data: c.body });
expect(res.status()).toBe(c.status);
if (c.field) {
const fields = (await res.json()).error.details.map((d: any) => d.field);
expect(fields).toContain(c.field);
}
});
}
test('malformed JSON -> 400 without internals', async ({ request }) => {
const res = await request.post(URL, { headers: { 'Content-Type': 'application/json' }, data: '{"name": "Asha", ' });
expect(res.status()).toBe(400);
expect(await res.text()).not.toMatch(/exception|stack|\.php/i);
});
test('wrong content type -> 415', async ({ request }) => {
const res = await request.post(URL, { headers: { 'Content-Type': 'text/plain' }, data: 'name=Asha' });
expect(res.status()).toBe(415);
});
Each row is a test with a readable name in the report, and adding a case is one line.
Negative Testing Checklist
- Every required field missing, one at a time, and all together.
- Every field: wrong type, null, empty, too short, too long, boundary values.
- Every enum: unknown value, wrong case.
- Every format: invalid email, phone, date, URL, UUID.
- Body level: malformed, empty, array, oversized, duplicate keys, unknown fields.
- Headers: wrong or missing Content-Type, Accept, auth.
- Cross-field and business rules.
- Unicode, special characters and injection strings.
- Responses: correct 4xx, clear field errors, no internals, no side effects.
Interview Questions: Negative and Boundary API Testing
What is negative testing in APIs?
Sending invalid, missing, malformed or unexpected input to verify the API rejects it gracefully with the correct 4xx status, a clear error message, no data changes and no server errors.
What is boundary value analysis? Give an API example.
Testing at the edges of allowed ranges, where off-by-one bugs occur. For an age field allowed from 18 to 100, test 17, 18, 100 and 101 and expect 422, 200, 200 and 422.
Which status codes should negative API tests expect?
400 for malformed or unparseable requests, 422 for validation rule failures, 415 for unsupported content type, 413 for oversized payloads, 405 for wrong methods, 406 for unsupported Accept, 401/403 for auth problems and 404 for unknown resources. Never 5xx.
What is the difference between equivalence partitioning and boundary value analysis?
Equivalence partitioning groups inputs that should behave the same and tests one value per group. Boundary value analysis tests values at and around the edges of those groups. They’re usually used together.
How do you test a numeric field in an API?
Valid values, boundaries (min-1, min, max, max+1), zero, negative numbers, decimals, very large numbers, numbers as strings, null, missing field, booleans, arrays and objects.
What should a good validation error response contain?
A specific 4xx status, a machine-readable error code, field-level details naming each failing field, a clear human-readable message, all errors at once, a consistent format and no internal implementation details.
Why test duplicate keys in JSON?
JSON parsers handle duplicates differently (first wins, last wins or error). If different layers interpret them differently, attackers can bypass validation, for example passing a small amount through a gateway while the backend processes a large one.
How do you confirm a rejected request had no side effects?
After the negative request, query the relevant resources, lists, balances or counters (or the database) to confirm nothing was created, changed or triggered, such as emails or payments.
Is a 500 error acceptable for invalid input?
No. Invalid client input should always produce a 4xx validation error. A 500 indicates missing validation or an unhandled exception and should be logged as a defect.
How do you organise many negative test cases in automation?
Use data-driven tests: a table of inputs, expected status and expected error field, looped by one test function, so each row appears as a named test and new cases are added as data.