BVA and Equivalence Partitioning for API Testing: Request Parameters, Headers, and Payloads
API testing benefits from the same systematic input analysis as any other testing, but the input space is more structured: path parameters, query parameters, headers, and request bodies — each with their own validation rules and boundary conditions.
This post applies BVA and equivalence partitioning specifically to REST API inputs.
The API Input Taxonomy
Before partitioning, classify API inputs:
Path parameters (required, typed):
GET /users/{userId}
GET /orders/{orderId}/items/{itemId}Query parameters (optional, typed, may have ranges):
GET /products?page=1&limit=20&min_price=10&max_price=100Request headers (typed, often with restricted value sets):
Authorization: Bearer <token>
Content-Type: application/json
Accept-Language: en-USRequest body (structured, may have nested objects with their own validation):
{
"name": "John Doe",
"age": 25,
"preferences": ["email", "sms"]
}File uploads (size, type, content constraints)
Each category has different partition structures.
Path Parameters
Path parameters are typically IDs or resource identifiers. Common partition structure:
Numeric ID partitions
For GET /users/{userId}:
| Partition | Representative | Expected |
|---|---|---|
| Valid existing ID | 12345 | 200 OK |
| Valid non-existing ID | 99999999 | 404 Not Found |
| ID = 0 | 0 | 400 or 404 |
| Negative ID | -1 | 400 Bad Request |
| Max int32 | 2147483647 | 400 or 404 |
| Max int64 overflow | 9223372036854775808 | 400 Bad Request |
| Non-numeric | "abc" | 400 Bad Request |
| SQL injection | "1; DROP TABLE users" | 400 Bad Request (no SQL execution) |
| UUID format (if wrong type) | "550e8400-e29b-41d4-a716-446655440000" | 400 Bad Request |
The overflow value (max int64 + 1) tests whether the API handles large numeric strings without crashing. Many APIs crash or produce wrong results when a numeric ID exceeds the integer type used internally.
UUID partitions
For GET /resources/{uuid}:
| Partition | Representative | Expected |
|---|---|---|
| Valid UUID (existing) | "550e8400-e29b-41d4-a716-446655440000" | 200 OK |
| Valid UUID (non-existing) | "00000000-0000-0000-0000-000000000000" | 404 Not Found |
| UUID format violation | "not-a-uuid" | 400 Bad Request |
| UUID with wrong version | "550e8400-e29b-11d4-a716-446655440000" | May accept or reject |
| Empty string | "" | 404 or 400 |
| Nil UUID | "00000000-0000-0000-0000-000000000000" | 404 (or special case?) |
The nil UUID partition is interesting: some systems treat it specially, some don't. Testing it reveals the system's behavior for this edge case.
Query Parameters
Query parameters are often optional, which adds a "missing" partition in addition to the usual value partitions.
Pagination partitions
For GET /products?page={page}&limit={limit}:
Page parameter (typically 1-indexed, must be positive):
| Partition | Value | Expected |
|---|---|---|
| Missing | (not provided) | Default behavior (page 1) |
| Page 1 | 1 | First page |
| Last valid page | (depends on total count) | Last page of results |
| Page beyond last | N+1 | Empty results or 404 |
| Page 0 | 0 | 400 Bad Request |
| Negative page | -1 | 400 Bad Request |
| Non-integer | "first" | 400 Bad Request |
| Very large page | 9999999 | Empty results or 400 |
Limit parameter (typically 1–100):
| Partition | Value | Expected |
|---|---|---|
| Missing | (not provided) | Default limit |
| Minimum valid | 1 | Returns 1 item |
| Maximum valid | 100 | Returns up to 100 items |
| Below minimum | 0 | 400 Bad Request |
| Above maximum | 101 | 400 or capped at 100 |
| Non-integer | "all" | 400 Bad Request |
| Very large | 1000000 | 400 or OOM risk |
The interaction between page and limit matters: page=100, limit=100 with a 50-item dataset should return empty results, not an error. Testing "page beyond last" requires knowing the total count — use a small dataset in tests to control this.
Filter parameter partitions
For GET /orders?status={status}&from_date={date}&to_date={date}:
Status filter (enum: PENDING, CONFIRMED, SHIPPED, DELIVERED, CANCELLED):
| Partition | Representative | Expected |
|---|---|---|
| Valid status | "SHIPPED" | Filtered results |
| Unknown status | "REFUNDED" | 400 or empty |
| Case variant | "shipped" | 400 or case-insensitive match |
| Multiple values | "PENDING,SHIPPED" | Depends on API spec |
| Empty | "" | 400 or all statuses |
| Missing | (not provided) | All statuses |
Date range (from_date / to_date):
| Partition | Representative | Expected |
|---|---|---|
| from_date only | from_date=2023-01-01 | Orders from date to now |
| to_date only | to_date=2023-12-31 | Orders up to date |
| Valid range | from=2023-01-01, to=2023-12-31 | Orders in range |
| Inverted range | from=2023-12-31, to=2023-01-01 | 400 Bad Request |
| Same day | from=to=2023-06-15 | Orders on that day |
| Invalid date | from_date="yesterday" | 400 Bad Request |
| Far future | to_date=2099-12-31 | All current orders |
The inverted range partition (from > to) is commonly missed. Many APIs silently return empty results instead of an error.
Request Headers
Headers have value-type partitions and also have the "missing required header" partition.
Authorization header partitions
For Bearer token authentication:
| Partition | Value | Expected |
|---|---|---|
| Valid token | "Bearer |
200 OK |
| Missing header | (not provided) | 401 Unauthorized |
| Wrong scheme | "Basic |
401 Unauthorized |
| Expired token | "Bearer |
401 Unauthorized |
| Malformed JWT | "Bearer not.a.jwt" | 401 Unauthorized |
| Token for different service | "Bearer |
401 Unauthorized |
| Token with wrong permissions | "Bearer |
403 Forbidden |
| Extremely long token | "Bearer " + "A" × 10000 | 400 or 431 |
The "wrong permissions" partition distinguishes 401 (unauthenticated) from 403 (authenticated but unauthorized). Many APIs return 403 for everything or 401 for everything — testing this partition verifies correct HTTP semantics.
Content-Type header partitions
For POST/PUT endpoints:
| Partition | Value | Expected |
|---|---|---|
| Correct type | "application/json" | 200 OK |
| Missing | (not provided) | 400 or 415 |
| Wrong type | "text/plain" | 415 Unsupported Media Type |
| With charset | "application/json; charset=utf-8" | Should work |
| Unknown type | "application/x-custom" | 415 |
Request Body Partitions
Complex request bodies need partitioning at multiple levels.
Structural partitions (JSON body)
For any JSON request body:
| Partition | Representative | Expected |
|---|---|---|
| Valid JSON | {"key": "value"} | 200 |
| Invalid JSON | "{key: value}" | 400 |
| Empty body | (no body) | 400 or treats as empty object |
| Empty JSON object | {} | 400 (missing required fields) |
| Extra fields | Valid + {"unknown_field": "x"} | 200 (ignore) or 400 |
| Null body | null | 400 |
| Array instead of object | [{"key": "value"}] | 400 |
| Very large body | Valid payload × 1000 | 413 or 400 |
Field-level partitions
For each field in the request body, apply the relevant EP rules based on type (string, numeric, date, enum, collection — as covered in the previous sections).
Additionally for required/optional:
| Partition | Description | Expected |
|---|---|---|
| All required fields | Complete valid body | 200 |
| Missing required field | Body with one required field omitted | 400 |
| Null required field | Required field set to null | 400 |
| All optional fields omitted | Only required fields | 200 (defaults apply) |
| Optional field set to null | Explicit null for optional | 200 (treated as absent) or 400 |
Testing every combination of missing required fields is O(2^n) — impractical. At minimum, test: each required field missing independently, and all required fields present.
Nested object partitions
For nested objects, apply partitions recursively:
{
"user": { // Required object
"name": "...", // Required field
"email": "...", // Required field
"address": { // Optional nested object
"street": "...", // Required if address present
"city": "..." // Required if address present
}
}
}Partitions for the address object:
- Address absent (valid — optional)
- Address present, fully specified (valid)
- Address present, street missing (invalid — required if address present)
- Address present as empty object {} (invalid — required fields missing)
- Address present as null (treat as absent, or error?)
Rate Limiting Boundaries
APIs with rate limiting have boundary conditions at the rate limit itself:
| Partition | Requests | Expected |
|---|---|---|
| Well under limit | 1 request | 200 |
| At limit - 1 | Limit - 1 requests | 200 |
| At limit | Limit requests | 200 |
| Just over limit | Limit + 1 requests | 429 Too Many Requests |
| Well over limit | 10× limit requests | 429 |
| After window reset | First request after window | 200 |
Rate limiting tests require time control — you need to know when the rate limit window starts and resets. Testing this against a real API requires either a mock rate limiter or timing the requests to a specific window.
Check that 429 responses include:
Retry-AfterheaderX-RateLimit-Limit,X-RateLimit-Remaining,X-RateLimit-Resetheaders- Clear error message
Idempotency Partitions
For idempotent operations (PUT, DELETE), test:
| Partition | Description | Expected |
|---|---|---|
| First operation | Normal execution | 200 or 201 |
| Duplicate operation (same payload) | Same request twice | 200 (second = no-op) or 204 |
| Duplicate with changed payload | Same ID, different data | 409 or 200 (update) |
| Operation on already-deleted resource | DELETE twice | 204 or 404 |
Idempotency bugs are common in distributed systems: a network timeout causes a retry, and the system incorrectly processes the same operation twice.
Documenting API Partitions
For each API endpoint, document the partition structure in a test plan:
Endpoint: POST /orders
Parameter: body.quantity (int)
Partitions:
- EP1 (invalid): quantity < 1 → 400 (test with: 0, -1)
- EP2 (valid): 1 ≤ quantity ≤ 100 → 201 (test with: 1, 50, 100)
- EP3 (invalid): quantity > 100 → 400 (test with: 101, 1000)
- EP4 (invalid): non-integer → 400 (test with: "five", 1.5)
- EP5 (invalid): missing → 400 (test with: field absent)This documentation makes partition coverage explicit and reviewable. It's also the specification for automated test generation.
Summary
BVA and EP for APIs follow the same principles as any input analysis: identify the behavioral regions, test one representative from each region, test the boundaries between regions.
The API-specific additions: path parameters need overflow and injection partitions; query parameters need "missing" partitions; headers need "wrong scheme" and "wrong permissions" partitions; request bodies need structural partitions in addition to field-level partitions.
The combination of systematic EP with BVA at the boundaries produces test suites that cover the full input space at reasonable cost — a fraction of the tests that exhaustive testing would require, while covering the failure modes that matter.