Test AI-Generated PATCH Handlers for Omitted vs. Null Fields

AI-generated illustration. Omission preserves the stored value; explicit null clears it. The handler must distinguish these two intentions rather than send both through the same fallback.
To test an AI-generated PATCH handler against requirements, define what each input means, send those inputs through the real handler, and check the persisted result.
For a nullable displayName, the distinction matters: an omitted field can mean “leave it alone,” while explicit null means “clear it.” Decide before implementation. A fallback expression can otherwise turn a requested clear into a successful-looking no-op.
Define the update contract
For this worked example, assume a JSON endpoint updates a profile in an isolated test database. The authenticated test caller may update that profile. Successful requests return 204 No Content, an example API decision, not a requirement imposed by PATCH itself.
Declare one requirement:
PATCH-DISPLAY-NAME-1: When
displayNameis omitted, preserve its stored value. When it is explicitlynull, store null. When it is a string, replace the stored value exactly, including an empty string and any surrounding whitespace.
This contract permits a cleared display name and treats "" as different from null. If your product requires a nonempty name, revise the requirement rather than letting the handler decide accidentally.
These are declared API semantics. Do not infer them from the HTTP method alone.
Use four independent cases
Seed displayName with "Ada" independently before every case. A non-null, nonempty starting value makes both clearing and empty-string replacement observable.
| Case | JSON request body | Expected stored displayName |
What it checks |
|---|---|---|---|
| Omitted | {} |
"Ada" |
Absence preserves the value |
| Explicit null | {"displayName":null} |
null |
A requested clear is persisted |
| Empty string | {"displayName":""} |
"" |
Empty string is not treated as absence |
| Nonempty string | {"displayName":" Grace "} |
" Grace " |
Replacement preserves the supplied whitespace |
All four cases expect 204. Compare stored values strictly, so null, "", and "Ada" cannot pass as interchangeable values.
The spaces around Grace expose unintended trimming. They do not establish correct handling of every possible string.
Give the coding agent a counterexample
Consider this illustrative JavaScript error:
const nextDisplayName = body.displayName ?? current.displayName;
Nullish coalescing selects the fallback for both undefined and null. For an ordinary parsed JSON object, an omitted displayName reads as undefined. The expression therefore preserves the old value for omission, but also for an explicit clear.
With the independently seeded value "Ada":
// Request body: { "displayName": null }
// Required next value: null
// Value produced by the fallback: "Ada"
Replacing ?? with || adds another mistake: an empty string also selects the old value.
The implementation must distinguish presence from value. An illustrative update-object pattern is:
// Assumption: body has already been validated as a JSON object.
const update = {};
if (Object.prototype.hasOwnProperty.call(body, "displayName")) {
const value = body.displayName;
if (value !== null && typeof value !== "string") {
throw new TypeError("displayName must be a string or null");
}
update.displayName = value;
}
This pattern omits the field from the update object only when it is absent. It includes explicit nulls and strings. It is not a complete handler or a tested implementation.
The handler still needs error mapping and persistence behavior, including support for requests that produce no changes. A later layer could also drop nulls or normalize strings, which is why the saved result matters.

AI-generated illustration. A successful response alone cannot establish correct null handling. Each acceptance case must read persistence afresh and compare the exact saved value with the declared contract.
Test the real handler and persisted state
A unit test can explain the fallback bug. It cannot establish that the endpoint honors the contract.
For each table row:
- Create an isolated fixture. Insert a test profile with
displayNameset to"Ada". Use a fresh profile or reset it independently for each case. Never use production credentials or live customer data. - Send the exact JSON payload. Exercise the actual PATCH route, request parsing, handler, and persistence code. Use the expected content type and valid test authorization. Do not mock the handler or database write for this acceptance check.
- Assert the response. Require
204for this example. A validation error that leaves the database unchanged must not pass the omission case. - Read from persistence. After the request completes, query the database afresh. Do not rely on the response body or an in-memory object the handler mutated.
- Assert the exact stored value. Compare against the table without trimming, coercion, or fallback logic in the assertion.
- Clean up the fixture. Scope cleanup to isolated test resources.
Keep the cases independent. If the null case starts with null already stored, a broken no-op implementation can pass. Shared state can also make results depend on execution order.
As a test-sensitivity exercise, temporarily substitute the ?? implementation on a local test branch using only the isolated database. The explicit-null case should fail. If it does not, inspect the fixture, route, and persisted-state assertion before trusting the suite. Restore the intended implementation afterward.
Keep a requirement-to-test record
Record what was checked alongside the results:
Requirement: PATCH-DISPLAY-NAME-1
Requirement revision: <document revision or commit>
Handler/code revision: <commit under test>
Acceptance-test revision: <commit containing the cases>
Environment: <isolated database and relevant configuration>
Independent seed for each case: displayName = "Ada"
Expected success status: 204
Cases: omitted / null / empty string / whitespace-bearing string
Evidence per case: payload, status, expected value, stored value, pass/fail
Known limits: authorization, concurrency, invalid-input behavior,
other initial states, and other fields are not established here.
If an agent changes null handling, the null case can flag a regression. If the test also changes to expect "Ada", the recorded requirement makes that drift visible.
A failing contract test calls for a decision: fix the implementation or explicitly revise the requirement. Do not weaken the assertion just to remove the failure.
Know what a passing result cannot prove
These cases provide evidence for four transitions through the tested handler and persistence path, not exhaustive conformance. They do not establish:
- Authorization correctness: An allowed test caller says nothing about whether forbidden callers are rejected.
- Concurrency safety: These sequential cases do not test for lost updates or transaction races.
- Complete validation: Arrays, numbers, malformed bodies, and unknown fields need separate decisions and tests.
- Every starting state: Each case starts from
"Ada", not from null or an empty string. - Absence of side effects: Checking
displayNamealone does not show that other fields remain unchanged.
The result is narrower and useful: repeatable checks that distinguish the declared behavior from a specific implementation mistake.
For a next step, see ThunderLang’s getting-started guide.