This page is for testers and QA engineers facing an API testing round, from a first QA job to a senior automation role. Most rounds start with what to check in an endpoint and how HTTP methods and status codes turn into test cases. Then they move to hands-on work in Postman and REST Assured, schema checks, negative and boundary tests, and auth. Senior rounds add contract testing, mocks, concurrency and a judgement call on release day. Each question shows what the interviewer is really checking, the shape of a strong answer and a short answer you can say out loud. Practise saying them, then swap in your own stories.
Search all questions by round, difficulty and level, or save the ones you want to practise.
Response: status code, headers like Content-Type, body values and the body shape.
Rules: business logic, validation of bad input, and clear error messages.
Around it: auth and permissions, side effects on stored data, and a response-time budget.
"A 200 only tells me the server didn't fall over. First I check the status is the one the spec promises, then the headers, at least Content-Type, and then the body: are the values right, not just present, and does the shape match the schema, with the right types and required fields. Then I test the rules. What happens with missing fields, wrong types, values out of range? I want a 4xx with a clear message, never a 500. Then security: no token, an expired token, another user's data. Then side effects: if it's a create, I read the record back and check nothing else changed. And I keep an eye on response time so a slow endpoint gets noticed early."
Treating a 200 status and a non-empty body as a passed test.
Speed and stability: no browser, no rendering, so tests run fast and flake less.
Reach: you can send input the UI would never allow, which is exactly what an attacker does.
Timing and focus: you can test before the UI exists, and a failure points straight at the backend.
"UI tests are slow and fragile because they depend on the browser, the page layout and timing. API tests skip all that, so I can run hundreds in a few minutes and they rarely flake. The bigger reason is reach. The UI limits what a user can send, like a dropdown with only valid values, but anyone can call the API directly with any value they like. So validation has to hold at the API, and only an API test proves it does. API tests also start earlier, as soon as the endpoint exists, before the screen is built. And when one fails, I know the problem is in the backend, not in a locator. I still keep a thin layer of UI tests for the real user journeys."
Saying API tests replace UI tests completely, or that client-side validation is enough.
Fields: missing, empty, null, wrong type, bad format, too long.
Request: malformed JSON, wrong Content-Type, unknown extra fields, empty body.
Rules and result: duplicate email; every case gets a clear 4xx and no record is created.
"I go field by field first. For each of name, email and age: leave it out, send it empty, send null, and send the wrong type, like age as the text 'twenty'. For email, bad formats like a missing at sign or spaces. For name, a very long string and odd characters. For age, negative, zero and a decimal. Then the request itself: broken JSON, an empty body, the wrong Content-Type, which should give 415, and unknown extra fields. Then the rules: registering an email that already exists, which should give 409 or whatever the spec says. For every case I check three things: a 4xx not a 500, a message that names the field, and that no user was created."
Checking only that the status is not 200, without checking the message or that nothing was saved.
Number edges: 0, 1, 2 and 99, 100, 101.
Text edges: 0, 1, 49, 50 and 51 characters, plus spaces only.
API extras: decimals, numbers sent as strings, null, and values past the integer range.
"For quantity I test just below, on and just above each edge: 0, 1 and 2, then 99, 100 and 101. The ones inside should pass, 0 and 101 should get a 400 with a clear message. For the name, empty, one character, 49, 50 and 51, and a name of only spaces. Then the cases only an API gets, because the UI would block them: quantity as 1.5, as the string '10', as null, negative, and a huge number past what an integer can hold, which sometimes crashes the parser and gives a 500. For the name, I also try characters like accented letters or emoji, because some systems count bytes instead of characters, so a 50-character name can be rejected."
Testing only one valid value and one invalid value per field.
Before: what the suite looked like and how often it failed for no real reason.
Causes: shared data, order dependence, hard-coded IDs, time-based asserts, environment issues.
Fixes and result: independent tests, own data, clean up, and a measurable drop in false failures.
"At my last company I inherited an API suite of a few hundred tests that failed randomly almost every night, so people had stopped trusting it. I tracked every failure for two weeks and the causes were boring. Tests shared the same user and fought over its data. Some only passed if another test ran first. A few had IDs hard-coded from a database that got refreshed. And some asserted on exact timestamps. I changed the rule so every test creates its own data with unique values and cleans up after itself, removed ordering between tests, and loosened time checks to ranges. Environment outages got their own label instead of counting as test failures. Within a month, false failures were rare and people started reading the reports again."
Fixing flakiness by adding retries and longer waits everywhere without finding the cause.
Gather: ask the developer for a quick walkthrough; read the story, the code change and existing clients.
Write down: expected requests, responses and rules as assumptions, and get them confirmed.
Test and push: explore, then build tests; ask for a proper spec so the next person isn't stuck.
"I don't wait, but I don't guess silently either. I ask the developer for ten minutes to walk me through the request, the fields and the error cases. I read the user story and, if I can, the code change and its unit tests. If the UI already calls it, I watch the real requests in the browser's network tab. Then I write down what I think the rules are, like required fields, limits and status codes, and send that list to the developer and product owner to confirm. While they reply, I explore: happy path, then bad input, then auth. Anything they correct becomes a test. And I raise the missing spec, because the next tester and every client team will hit the same wall."
Treating whatever the endpoint currently does as the expected behaviour.
GET: changes nothing; filters, not found, and the same answer when called twice.
POST: creates; check the code, read it back, and see what a double submit does.
PUT and PATCH: PUT replaces the whole order, PATCH changes only the fields sent.
DELETE: the order is gone, a second delete behaves as the spec says, related data is handled.
"Each method makes a promise, and I test the promise. GET must not change anything, so I call it, check the data, call it again and make sure nothing moved. I also test filters and an ID that doesn't exist. POST creates an order, so I check the status and the returned ID, read it back with a GET, and try a double submit to see if I get two orders. For PUT, the body replaces the whole order, so I check what happens to fields I leave out and that sending the same PUT twice gives the same result. PATCH should change only the fields I send, so I verify the others stayed the same. For DELETE, I check the order is really gone, what a second delete returns, and what happens to its line items. I also try an unsupported method and expect 405."
Treating PUT and PATCH as the same thing, or never checking what a repeated call does.
Contract first: the spec decides; a mismatch with the spec is a bug, a gap in the spec is a question.
Usual codes: 201 for create, 204 or 200 for delete, 400 or 422 for bad input, 404 for a missing record.
Never: a 5xx caused by bad client input.
"It depends on the contract. If the spec says create returns 201, then 200 is a bug, because clients may check the code. If the spec says nothing, I raise it as a question rather than a defect, and suggest 201 since that's the standard code for a successful create, often with a Location header pointing to the new record. For delete, I usually expect 204 with no body, or 200 if it returns something. For bad input, a 400, or 422 if the team uses it for validation errors, plus a message that says which field is wrong. A missing record should be 404. The one thing I always log as a bug is a 500 caused by input the client sent. That means the server didn't validate it."
Accepting any 2xx as a pass, or treating a 500 on bad input as acceptable.
Collection: a folder of saved requests, scripts and tests that can be run and shared as one unit.
Environment: a named set of variables like baseUrl and credentials, one per dev, staging and so on.
Variables: written as double curly braces in requests; the narrowest scope wins when names clash.
"A collection is a group of saved requests, usually in folders by feature, and it can hold its own scripts and tests, so I can run the whole thing in one go. An environment is a set of variables for one target, like dev or staging, holding things like baseUrl, a client ID and the current token. In the requests I write the URL as baseUrl in double curly braces plus the path, so switching from dev to staging is just picking another environment. Tokens get set by a script after login, not pasted in by hand. Postman has several scopes, like global, collection, environment and local, and when two have the same name the narrowest one wins. I keep real secrets out of anything I share or export."
Hard-coding URLs and tokens into every request, or sharing a collection with live credentials in it.
What: JavaScript that runs before the request is sent, at request, folder or collection level.
Typical uses: fetch or refresh a token, build unique data, compute a signature or timestamp.
Pattern: store the result in a variable that the request then uses.
"A pre-request script is JavaScript that runs just before a request goes out. It can sit on one request, a folder or the whole collection. My most common use is auth. At collection level I check whether the token is missing, and if so I call the token endpoint with pm.sendRequest, save the access token into an environment variable, and every request uses it in its Authorization header. Another use is unique test data: if the API rejects duplicate emails, I build an email with a timestamp so the test can run again and again. I've also used it to compute a request signature when an API needed one. The idea is always the same: work something out, store it in a variable, and let the request use it."
if (!pm.environment.get("token")) {
pm.sendRequest({
url: pm.environment.get("baseUrl") + "/auth/token",
method: "POST",
header: { "Content-Type": "application/json" },
body: {
mode: "raw",
raw: JSON.stringify({
clientId: pm.environment.get("clientId"),
clientSecret: pm.environment.get("clientSecret")
})
}
}, function (err, res) {
if (err) { console.log(err); return; }
pm.environment.set("token", res.json().access_token);
});
}
pm.variables.set("email", "qa+" + Date.now() + "@example.com");
Copying a fresh token into every request by hand before each run.
Structure: one pm.test per check, with a name that says what failed.
Status and time: pm.response.to.have.status and pm.response.responseTime.
Body: parse with pm.response.json and assert with pm.expect.
"Tests in Postman are JavaScript that run after the response comes back. I write one pm.test per check so the report tells me exactly which one failed. The first asserts the status is 200. The second checks pm.response.responseTime is under a budget we agreed, say 800 milliseconds, though I keep that loose because a single timing is noisy. The third parses the body with pm.response.json and checks a real value, like the order's status being 'confirmed', not just that the field exists. I'd usually add a check on the Content-Type header too. Because these run in the collection runner and in the command line, the same tests work locally and in the pipeline."
pm.test("status is 200", function () {
pm.response.to.have.status(200);
});
pm.test("responds within 800 ms", function () {
pm.expect(pm.response.responseTime).to.be.below(800);
});
pm.test("order is confirmed", function () {
const body = pm.response.json();
pm.expect(body.status).to.eql("confirmed");
pm.expect(body.items).to.be.an("array").that.is.not.empty;
});
Putting every check in one big test, or asserting only that the body is not empty.
Capture: in the create request's tests, read the ID from the response and save it to a variable.
Reuse: later requests use the variable in the path or body.
Order and cleanup: run in sequence, and end by deleting what the flow created.
"In the tests of the create request, I first assert it returned 201, then read the new ID from the response body and save it as a collection variable called orderId. The next requests use orderId in double curly braces in their URLs, so the get, update and delete all hit the record I just made. The collection runner runs requests in order, so the chain works in one run. I assert at every step, because if the create fails, I want the flow to fail loudly, not quietly test a stale ID from last time. At the end, the delete cleans up and a final get checks for a 404. That way the flow leaves no junk data behind and can run again."
// Tests tab of POST /orders
pm.test("order created", function () {
pm.response.to.have.status(201);
});
const created = pm.response.json();
pm.collectionVariables.set("orderId", created.id);
// Next request URL: {{baseUrl}}/orders/{{orderId}}
Copying IDs between requests by hand, or relying on IDs that exist only in one environment.
Runner: export the collection and environment and run them with Newman or the Postman command line.
Data: a CSV or JSON file gives one iteration per row; requests read the columns as variables.
Pipeline: a failed test gives a non-zero exit code and fails the build; publish a JUnit report.
"I export the collection and the environment file, or pull them from the Postman API, and run them with Newman in the pipeline. Newman exits with a non-zero code when any test fails, so the build fails without extra work. I add a JUnit reporter so the CI tool shows each test result. For data, I pass a CSV or JSON file. Each row becomes one iteration, and the requests read the columns as variables, like email and expected status, so one request can cover valid and invalid cases. Secrets don't live in the exported environment. The pipeline injects them as variables at run time. I keep a short smoke collection for every build and the full run for nightly."
newman run orders.postman_collection.json \
-e staging.postman_environment.json \
-d order-cases.csv \
-r cli,junit --reporter-junit-export results/api-tests.xml
Committing an environment file with real secrets to the repository.
Request: check the environment selected, variable values and the real request in the Postman console.
Environment: auth server, deployed version, config flags and certificates may differ.
Data: IDs and records that exist only in dev; make tests create their own data.
"First I look at which tests fail and the exact error, and I open the Postman console to see the real request that went out, because the variables might not resolve the way I think. A stale or missing environment variable is the most common cause. Then I compare the environments: is staging on a different build, a different auth server, feature flags turned off, or a certificate issue? Then data: if a test uses order 1001 and that order only exists in dev, it'll fail in staging for no good reason. That one I fix in the test by creating the data first. If it's a real difference in behaviour, I check with the developer whether staging or dev is right before I log a bug."
Saying 'staging is broken' without comparing the actual requests and responses.
given: base URI, path parameters, headers and auth.
when: the HTTP call itself.
then: status, content type and body assertions with Hamcrest matchers.
"REST Assured reads like a test case. In given, I set the base URI and the path parameter for the ID, plus any auth header. In when, I make the GET call with the path template. In then, I assert the status is 200, the content type is JSON, and use a path expression to check the name field equals the name I expect, using a Hamcrest matcher. If the body is nested, the path follows the JSON, like address.city. In a real framework the base URI and auth would come from a shared request specification, not every test. And I'd make sure the user exists first, ideally by creating it in the setup, so the test doesn't depend on data someone else might delete."
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.equalTo;
import io.restassured.http.ContentType;
import org.junit.jupiter.api.Test;
class UserApiTest {
@Test
void getUserReturnsName() {
given()
.baseUri("https://api.example.com")
.pathParam("id", 7)
.when()
.get("/users/{id}")
.then()
.statusCode(200)
.contentType(ContentType.JSON)
.body("name", equalTo("Asha"));
}
}
Checking only the status code, or not knowing where the base URI and auth should be shared.
Send: set the content type and pass a JSON string or a Java object as the body.
Extract: assert first, then extract the value with a JSON path.
Reuse: feed it into the next request and verify the data round-trips.
"I set the content type to JSON and pass the body. For small tests a string is fine, but in a framework I pass a plain Java object, and REST Assured serialises it, as long as Jackson or Gson is on the classpath. After the call I assert the status is 201 first, then use extract and the JSON path to pull out the new ID. I read it as a string so I don't care whether the API sends it as a number or text. Then I use that ID as a path parameter in a GET and check the email I sent came back exactly the same. That round trip proves the data was really saved, not just echoed back by the create call."
String id =
given()
.contentType(ContentType.JSON)
.body("{\"name\":\"Asha\",\"email\":\"asha@example.com\"}")
.when()
.post("/users")
.then()
.statusCode(201)
.extract().jsonPath().getString("id");
given()
.pathParam("id", id)
.when()
.get("/users/{id}")
.then()
.statusCode(200)
.body("email", equalTo("asha@example.com"));
Extracting values before asserting the status, so a failed create gives a confusing error later.
What: a JSON document that describes the allowed shape: types, required fields, allowed values.
Catches: a field that vanished, changed type, turned null or appeared unexpectedly.
Limits: it proves shape, not correctness; you still assert the actual values.
"A JSON schema describes what a valid response looks like: which fields are required, their types, which can be null, and whether extra fields are allowed. Field checks only look at the three or four things I thought of. A schema checks everything at once, so if a developer renames a field, changes an ID from a number to a string, or a nested field quietly becomes null, the test fails even though my value checks still pass. Setting additionalProperties to false also catches new fields sneaking out, which can be a data leak. But a schema can't tell me the price is right. So I use both: a schema for shape, and value assertions for the business rules."
const schema = {
type: "object",
required: ["id", "name", "email"],
properties: {
id: { type: "integer" },
name: { type: "string", minLength: 1 },
email: { type: "string" },
phone: { type: ["string", "null"] }
},
additionalProperties: false
};
pm.test("user matches schema", function () {
pm.response.to.have.jsonSchema(schema);
});
Believing a response is correct because it matches the schema.
Read back: fetch it with a separate GET and compare every field, including defaults.
Other views: check it shows in list and search endpoints and, where allowed, in the database.
Side effects: events, emails or audit entries fired, and nothing else changed by mistake.
"The echo in a create response can come straight from the request, so it proves very little. I fetch the record with a separate GET and compare every field I sent, plus the ones the server fills in, like the ID, created time and default status. I check formats too, like whether dates come back in the agreed time zone and text isn't trimmed or cut short. Then I check it appears where it should, in the list endpoint and in search. If I have read access to the database, I check the stored row as well. Finally I look at side effects: was the welcome email queued, the audit entry written, and did nothing else change, like a count on another record."
Treating the create response body as proof the data was stored.
Token problems: none, malformed, expired, tampered or signed wrongly, from another environment.
Permissions: valid token but wrong role or scope, and a user reaching another user's data.
Leaks: tokens never in URLs, logs or error bodies; logout and revocation behave as designed.
"I split it into two groups. First, is the caller who they claim to be. I send no token, a malformed one, an expired one, one with a single character changed so the signature breaks, and one from another environment. All of those should get 401 and no data. Second, is the caller allowed. I use a valid token with the wrong role or missing scope and expect 403. Then the important one: user A's token on user B's record, which must fail too. I also check the edges: after logout or password change, does the old token still work, and is that what the team intended? And I check tokens never show up in URLs, logs or error messages."
Testing only a valid and a missing token, and never trying one user against another user's data.
Setup: two real test users; A creates the order, B tries to reach it.
Every door: GET, PUT, PATCH, DELETE, nested paths, list filters and IDs inside request bodies.
Expect: 403 or 404 as agreed, no data in the body, and the order unchanged afterwards.
"This is broken object-level authorisation, and it's one of the most common serious bugs in APIs, because the server checks that you're logged in but not that the object is yours. I create two test users. A creates an order, then B, with a valid token, tries every way in: GET, PUT, PATCH and DELETE on that order ID, nested paths like its items or invoice, list endpoints with a filter on A's ID, and any request body that takes an order ID, like a refund request. I expect 403 or 404, whatever the team agreed, and I check the body leaks nothing. Then I read the order as A to prove it didn't change. I automate this for every endpoint that takes an ID, because it's easy to forget on new ones."
Assuming a logged-in user can only see their own data because the UI only shows them their own.
Input: extra fields like role or isAdmin, injection characters, huge or odd payloads.
Output: responses that expose internal fields, and errors that show stack traces or queries.
Abuse: no rate limit on login or one-time codes, plain HTTP accepted, sensitive data in URLs.
"Quite a lot, actually. On input, I try mass assignment: I add fields the client shouldn't set, like role set to admin or a verified flag, and check the server ignores them. I send quotes and other injection-style characters and a very large payload, and anything that gives a 500 gets a closer look. On output, I read full responses for things that shouldn't be there, like password hashes, internal IDs or other users' emails. I trigger errors and check they don't leak stack traces or SQL. On abuse, I try many wrong logins or one-time codes quickly and expect to be slowed or blocked. And I check plain HTTP is refused and secrets never go in the URL. Anything deeper I pass to the security team."
Saying security is entirely the security team's job, or trying risky attacks on production without permission.
Idea: the consumer records what it sends and which response fields it relies on; that is the contract.
Check: the provider replays the contract against its real code on every build.
Difference: it checks the agreement between two services, not the business logic.
"Contract testing checks the agreement between two services. In the consumer-driven style, the consumer team writes tests against a mock provider, and those tests produce a contract: the requests they'll send and the fields they actually use in the response. The provider then runs that contract against its real code on every build. If a provider change would break a consumer, like renaming a field the consumer reads, the provider's build fails before anything ships. Pact is a well-known tool for this. It's different from my functional tests. Those check the business rules, like whether the discount is right. Contract tests only check shape and interactions, so they're fast and don't need every service running together."
Describing contract testing as just checking the response against a schema file.
When: the real service is not built, unstable, slow, costly per call, or cannot easily produce errors.
How: tools like WireMock or a Postman mock server return set responses for set requests.
Risk: the mock drifts from reality; contract tests and some real runs keep it honest.
"I use a mock when the real service gets in the way of testing my own API. Maybe it isn't built yet, or the shared sandbox goes down twice a day, or every call costs money, like a payment or SMS provider. The biggest reason is error cases. It's hard to make a real partner return a 503 or time out on demand, but a mock does it every time. With a tool like WireMock I say: for this request, return this status and body, or wait five seconds. The risk is that the mock lies. The real service changes a field and my mock doesn't, so my tests pass and production breaks. So I pair mocks with contract tests, and keep a smaller set of tests against the real service."
Running every test against mocks and never against the real integration.
Simulate: point the API at a mock that delays, returns 500, drops the connection or sends bad JSON.
Expect: a timeout within the agreed limit and a clear error, not a hang or a leaked stack trace.
State: no partial writes, limited retries, and recovery once the dependency is back.
"I point our API at a mock of the stock service in a test environment, so I control how it fails. Then I run four cases. A long delay: our API should give up after its configured timeout and return an agreed error, like 503 or 504, not hang the client. A 500 from the stock service: same idea, a clear error, no stack trace passed through. A dropped connection. And a 200 with broken or unexpected JSON, which often slips through. In each case I check that we didn't half-finish anything, like reserving an order without stock. If there are retries, I count the calls on the mock to confirm they're limited. Finally I bring the mock back to normal and check the API recovers without a restart."
// Slow: longer than our API's own timeout
stubFor(get(urlEqualTo("/stock/42"))
.willReturn(aResponse()
.withStatus(200)
.withFixedDelay(10000)));
// Dropped connection
stubFor(get(urlEqualTo("/stock/43"))
.willReturn(aResponse()
.withFault(Fault.CONNECTION_RESET_BY_PEER)));
// A 200 with broken JSON
stubFor(get(urlEqualTo("/stock/44"))
.willReturn(aResponse()
.withStatus(200)
.withBody("{\"qty\": ")));
// After the slow case: retries stopped at three calls
verify(3, getRequestedFor(urlEqualTo("/stock/42")));
Only testing the happy path because the dependency is usually up.
Mismatch: what the docs said and what the API really did.
Resolve: show evidence, and agree with developers and product which one is right.
Prevent: update the spec, and add a test or schema check that catches drift next time.
"In a project last year, the docs said the date of birth came back as a date string, but the API returned a number, a timestamp. The mobile team had coded against the docs, so their screen showed nonsense. I didn't just change my test to match the API. I raised it with the request and response, and asked the developer and the product owner which was meant to be true. We agreed the docs were right, since other clients depended on them, so the API was fixed. Then I added a schema check generated from the spec to our test run, so any field that changed type would fail the build instead of reaching another team."
Quietly changing the test to match whatever the API returns.
Timing: response-time budgets on key calls, judged over several runs, not one.
Growth: how time and payload size change as the data grows, and whether lists are paginated.
Small bursts: a handful of parallel calls to spot errors, timeouts and locking early.
"I keep it simple and early. I add a response-time budget to the important calls, but I judge it over several runs and look at the slow end, because one timing on a shared server is noise. I watch payload sizes: a list endpoint returning thousands of records with every field is a problem long before any load test. I also test with more data, like a customer with ten orders versus a thousand, because if time grows steeply, there's often a query per item hiding inside. And I fire a small burst of parallel requests to see if anything errors or locks up. None of this replaces a real load test with proper workload modelling, but it catches the cheap problems before they get expensive."
Calling a single fast response in Postman proof that an endpoint performs well.
Setup: one seat left, several users ready with valid tokens.
Fire together: send the bookings in parallel from code or a load tool, not one by one.
Expect and repeat: exactly one success, the rest a clear conflict, stock never negative; run it many times.
"Sequential tools like a normal collection run can't find this, because the requests never overlap. I set up a show with one seat left and several users, then send the booking requests at the same moment from a small piece of code using a thread pool, or from a load tool. The correct result is exactly one 201 and every other request a 409 or whatever conflict code we agreed. Then I check the data itself: one booking in the database, and the seat count at zero, not minus one. Race conditions are intermittent, so one clean run proves little. I repeat it many times, and I try variations like the same user clicking twice, because double submits cause the same bug."
ExecutorService pool = Executors.newFixedThreadPool(5);
List<Callable<Integer>> calls = new ArrayList<>();
for (int i = 0; i < 5; i++) {
calls.add(() -> given().spec(authSpec)
.post("/shows/9/seats/A1/book")
.statusCode());
}
List<Integer> codes = new ArrayList<>();
for (Future<Integer> f : pool.invokeAll(calls)) {
codes.add(f.get());
}
pool.shutdown();
assertEquals(1, Collections.frequency(codes, 201));
assertEquals(4, Collections.frequency(codes, 409));
Testing a race condition by sending two requests one after the other.
Input: missing validation that turns bad input into 500s or bad saved data.
Contract: wrong status codes, renamed or retyped fields, nulls where the spec says required.
Access and data: reaching other users' records, leaked fields, time zone and pagination slips.
"The top one is missing validation. Bad input gets through and either crashes with a 500 or gets saved, and then breaks something later. Next is wrong status codes, like 200 for errors or 500 for a not-found. Then contract drift: a field renamed, an ID that switches from number to string, or a required field coming back null. Then access bugs, where one user can read or change another user's data by changing an ID. Data leaks are common too, like internal fields or password hashes in a response. And then the quiet ones: dates in the wrong time zone, pagination that skips or repeats records at the page edge, and error messages that are unhelpful or expose a stack trace."
Only naming UI-style bugs like typos, or not being able to give a single concrete example.
Situation: the feature and why the UI hid the problem.
Action: the request you sent, what came back, and how you showed the impact.
Result: the fix, and the test you added so it stays fixed.
"At my last company we had a checkout flow, and the UI tests were all green. When I tested the checkout API directly, I noticed the request body carried the item price, and I wondered if the server trusted it. I changed the price to one unit and the order went through at that price. The UI never let anyone edit the price, so no UI test could find it. I wrote it up with the exact request, the response and the saved order, marked it critical, and walked the developer through it that afternoon. The fix was to ignore the client price and look it up on the server. I added API tests that tamper with prices and quantities, and we reviewed other endpoints for fields the client shouldn't control."
A story about a cosmetic bug, or one where you found the issue but did nothing to stop it coming back.
Triage: rerun each alone, reproduce by hand, read the actual request and response.
Classify: product bug, test bug or environment and data issue, each with evidence.
Report: tell the release owner the risk plainly; fix or mark test bugs with a reason, never delete them quietly.
"I don't argue in general terms. I take each failure separately and fast. I rerun it on its own and reproduce the call by hand with the same request. Then I sort it. If the test is wrong, say an old expected value, I fix it or mark it with a clear reason and a ticket, and I tell the developer they were right. If it's environment or data, I show that. If it's a real product bug, I write down the request, the response, what users would see and how often, and take it to whoever owns the release decision. It's their call whether to ship with a known issue, but it should be an informed call. What I won't do is keep rerunning until it goes green."
Disabling the failing tests to get a green run, or blocking the release without checking what the failures are.
ClapAssist is an AI interview assistant for Mac and Windows. It listens to the interview on your computer and shows you what to say, in short lines you can read while you talk. Your resume and notes are never stored on our servers. It stays out of screen share on every plan; only you can see it.