Test Case Design and Edge Case Analysis Questions
Systematically deriving the cases, inputs, and conditions most likely to expose defects. Covers formal test-design techniques (equivalence partitioning, boundary value analysis, decision tables, state transitions, and pairwise/combinatorial design) and writing clear, maintainable test cases with documented expected results. Also covers the edge-case mindset: boundary conditions, invalid and unexpected inputs, corner cases, and the attention to detail that anticipates failures when validating complex behavior.
Describe a structured checklist or template you would use to enumerate boundary and off-by-one conditions for a new feature. Include at least ten checklist items that cover ranges (inclusive/exclusive), minimum/maximum values, zero and single-element cases, negative and sentinel values, date/time boundaries, integer limits, floating-point boundaries, indexing, and I/O limits. Explain how you'd apply this checklist during requirements review and when writing test cases.
Sample Answer
Direct answer
A reusable boundary/off-by-one checklist should walk through every dimension a value or collection can have an edge at: the range itself (inclusive/exclusive), the extremes (minimum/maximum), the degenerate cases (zero and single-element), unusual-but-valid values (negative numbers, sentinel values), and the domains where boundaries are especially easy to get wrong (dates/times, integer limits, floating-point precision, indexing, and I/O size limits).
Structured elaboration: the ten-plus checklist items
- Range inclusivity: for every stated range ("between X and Y"), confirm explicitly whether both ends are included, and test the boundary value itself, not just values near it.
- Minimum value: the smallest value the field/collection is documented to accept.
- Maximum value: the largest value the field/collection is documented to accept.
- Just below minimum / just above maximum: the immediately-invalid neighbors, to confirm the boundary is actually enforced, not off by one.
- Zero: for any numeric field, does zero mean 'none', 'unlimited', or an ordinary valid value? This is frequently ambiguous and worth pinning down explicitly.
- Single-element case: for any collection, list, or array, the one-element case often reveals loop or indexing bugs invisible in either the empty or many-element case (e.g. a loop that assumes at least two elements to compare adjacent pairs).
- Negative values: even for a field that 'should never' be negative, confirm what actually happens if it is, since the type system alone rarely prevents it.
- Sentinel values: reserved special values used to mean something other than their literal value (-1 for 'not found', 999999 for 'unknown', an empty string for 'unset'); confirm the sentinel doesn't collide with a legitimate value.
- Date/time boundaries: month-end, year-end, leap years, daylight-saving-time transitions, and the exact instant of a stated cutoff (e.g. "expires after 30 minutes" needs a test at exactly 30:00, not just 29:59 and 30:01).
- Integer limits: the type's maximum/minimum representable value (e.g. 2^31-1 for a 32-bit signed integer), and what happens one step beyond it.
- Floating-point boundaries: values very close to zero, very large magnitudes, and the classic non-exact-representation trap (does an equality check tolerate representation error, or not).
- Indexing: the first index, the last index, and one-past-the-last-index (a frequent off-by-one source in loop bounds).
- I/O limits: the maximum request/response/file size a system will accept, and what happens exactly at and just past that limit.
Worked example: applying the checklist during requirements review
During a requirements review for a new 'bulk import' feature accepting up to 10,000 rows per file, walking this checklist against the requirement surfaces several questions the requirement itself doesn't answer: is 10,000 inclusive (does row 10,000 succeed)? What happens with a 0-row file (empty, item 6)? Is there a MINIMUM row count, or is 1 row valid (item 2/6)? What happens to row 10,001 in a same-request bulk import (item 4)? These become explicit, answered acceptance criteria BEFORE implementation starts, rather than ambiguities discovered during test-case writing after the fact, which is a meaningfully cheaper time to resolve them.
Applying it when writing test cases
Once the requirement is unambiguous, each checklist item that applies to the feature becomes a candidate test case directly: the checklist's job at this stage shifts from 'surface ambiguity' to 'ensure nothing on this list was silently skipped', functioning as a coverage audit against the test suite that already exists rather than a fresh brainstorm every time.
Trade-offs & pitfalls
A checklist applied mechanically to every field regardless of relevance produces test-case bloat (testing floating-point-precision boundaries on a field that is always an integer wastes effort); the checklist's value is in prompting the QUESTION for every dimension, with the judgment to skip items that are genuinely inapplicable to a given field, not in guaranteeing every item becomes a test case every time.
List edge cases and failure modes to consider when implementing file uploads to the backend: zero-byte files, maximum allowed size exceeded, partial uploads due to network drop, streaming memory blowup, malicious filenames, and content-type mismatches. How would you write an integration test to simulate a partial upload and assert correct cleanup or resume behavior?
Sample Answer
Direct answer
File-upload edge cases span three failure categories: malformed or extreme content (zero-byte files, over-size files, streaming memory blowup on very large files), interrupted transport (partial uploads from a dropped network connection), and hostile input (malicious filenames, content-type mismatches), and the highest-value integration test simulates a partial upload and asserts the system cleans up the incomplete artifact rather than leaving orphaned data.
Structured elaboration
- Zero-byte file: the upload succeeds at the transport layer but the resulting file has no content; the system must decide and enforce whether an empty file is valid (many business contexts say no) rather than silently accepting it.
- Maximum allowed size exceeded: must be rejected with a clear, early error (ideally before the whole file transfers, via a Content-Length check) rather than accepting the full transfer and only then rejecting it, which wastes bandwidth and time.
- Streaming memory blowup: an implementation that buffers the entire file in memory before processing it can be forced into an out-of-memory condition by a large-but-under-the-nominal-limit file if the limit check itself happens too late or is missing on a different code path (e.g. a chunked-transfer-encoding request that never declares Content-Length).
- Malicious filenames: filenames containing path-traversal sequences (
../../etc/passwd), null bytes, or unusual encodings must be sanitized or rejected before the filename is ever used to construct a filesystem path, never trusted as literal path input. - Content-type mismatches: a file whose extension claims
.jpgbut whose actual bytes are something else (a script, or a different file format) must be validated by content sniffing, not just the client-supplied extension or MIME-type header, since both are attacker-controlled.
Worked example: integration test for a partial upload
def test_partial_upload_is_cleaned_up(upload_service, tmp_storage):
upload_id = upload_service.start_upload(filename="report.pdf", declared_size=10_000_000)
# simulate a network drop after only 30% of the bytes arrive
upload_service.receive_chunk(upload_id, data=b"x" * 3_000_000)
upload_service.simulate_connection_drop(upload_id)
# assert the system does NOT expose a partial file as if it were complete
assert upload_service.get_status(upload_id) == "incomplete"
assert not tmp_storage.has_committed_file("report.pdf")
# assert cleanup: after the configured retention window, the partial artifact is removed
upload_service.run_cleanup_sweep(older_than_seconds=0)
assert not tmp_storage.has_temp_artifact(upload_id)
# assert resume behavior: the client can either resume from the last committed chunk
# or must restart, and the API's documented contract for which one applies is what
# the test actually pins down (this example asserts a resume-from-offset contract)
resumed = upload_service.resume_upload(upload_id, filename="report.pdf")
assert resumed.resume_offset == 3_000_000
The test's structure matters as much as its assertions: it exercises three distinct states (in-progress, post-drop, post-cleanup) rather than a single before/after snapshot, because a partial-upload bug frequently lives specifically in the TRANSITION between those states (e.g. a race where cleanup runs before the drop is even detected, or a resume that silently restarts from zero instead of the last committed offset, wasting the bytes already transferred).
Trade-offs & pitfalls
A common gap is testing the size limit only against the DECLARED size in a header, never against the ACTUAL bytes received; a client can lie about Content-Length, and a server that trusts it exclusively can still be driven into the memory-blowup scenario by a request that declares a small size but streams far more. The resume-vs-restart contract above is also a real design decision, not a given: if the system does not actually support resuming from an offset, the test should instead assert that a resume attempt cleanly restarts rather than silently corrupting a half-written file by appending to it.
Define the term 'edge case' (and 'corner case') in the context of software testing. Why does systematically identifying them matter more than testing only the happy path? Give at least eight concrete categories, spanning at least three different domains (a generic input-validation example, a production/reliability example, and a data or ML-pipeline example).
Sample Answer
Direct answer
An edge case (or corner case, when two or more boundary conditions intersect) is an input, state, or condition at the extreme or unusual end of what a system is expected to handle, distinct from the 'happy path' of typical, well-formed usage; systematically identifying them matters because production traffic and adversarial users reliably generate exactly these unusual conditions, while happy-path testing alone only proves the system works when everything goes as expected, which is rarely where real defects live.
Structured elaboration: eight categories, spanning multiple domains
- Empty/null: an empty list, a null field, a zero-length string. Example (general software): a search function called with an empty query string.
- Boundary/max-min: values exactly at, or one step past, a defined limit. Example (backend): a pagination
page_sizeparameter at exactly the server-enforced maximum. - Zero/negative: values a numeric field technically accepts as a type but that may be nonsensical for the domain. Example (SRE/production): a negative value in a counter that should only ever increase, signaling either overflow or a bug in the decrement logic.
- Duplicate: repeated values where uniqueness might be silently assumed. Example (general software): two items with the same ID in a list a system expects to be de-duplicated upstream.
- Malformed/invalid type: input that is the wrong shape or type entirely. Example (backend): a JSON field expected to be an integer arriving as a string or an array.
- Out-of-order/concurrent: events or requests arriving in an unexpected sequence, or overlapping in time. Example (SRE/production): a delivery-confirmation event for a message arriving before the message-sent event, due to network reordering.
- Very large/very small scale: inputs at a magnitude far outside typical testing. Example (data/ML pipeline): a categorical feature with hundreds of millions of unique values (e.g. a raw user ID) fed into a one-hot encoder, which can silently exhaust memory.
- Environment/locale-specific: behavior that only manifests under a specific timezone, locale, or platform. Example (general software): a date-parsing function that behaves correctly in the US locale but misinterprets day/month order elsewhere.
Worked example: why happy-path testing alone misses these
A login form tested only with a valid, well-formed email and a correct password will pass every happy-path test while shipping with a null-pointer crash on an empty password field, an infinite spinner on a 10,000-character email, or a silent security bypass on a SQL-injection-shaped username, none of which a happy-path suite would ever exercise, because by construction happy-path tests only feed the system inputs the developer already expected to work.
Trade-offs & pitfalls
Treating 'edge case' as synonymous with 'rare' is a common misconception: an empty list or a zero value is often one of the MOST common real-world inputs (a brand-new user's empty cart, a freshly-created account with no activity yet), not a rare corner case, which is exactly why the empty/null category above is listed first, not last; conflating 'edge case' with 'unlikely' leads teams to systematically under-test the cases that actually occur most often in a real user base's earliest interactions with a feature.
Given the requirement: 'Users can reset their password by requesting a reset link to their registered email; the link expires after 30 minutes and becomes unusable after one successful use', derive a set of test cases that map directly to the acceptance criteria. Provide a simple requirements-to-test traceability matrix (mapping requirement ID to one or more test case IDs) and include positive, negative, and edge cases.
Sample Answer
Direct answer
Derive test cases directly from each distinct clause of the requirement (a valid reset flow, the 30-minute expiry, and the single-use restriction), and build a small traceability matrix mapping each requirement clause to the specific test IDs that verify it, so coverage of the stated acceptance criteria is explicit rather than implied.
Structured elaboration: traceability matrix
| Req ID | Requirement clause | Test case IDs |
|---|---|---|
| R1 | User can request a reset link to their registered email | TC1, TC2, TC3 |
| R2 | Link expires after 30 minutes | TC4, TC5, TC6 |
| R3 | Link becomes unusable after one successful use | TC7, TC8 |
Worked example: the test cases themselves
- TC1 (positive, R1): Request a reset link for a registered email. Expected: an email is sent containing a valid, unique link; the response does not reveal whether the email was previously registered in a way that differs based on true/false (a common security-adjacent edge case implied by 'registered email').
- TC2 (negative, R1): Request a reset link for an email that is NOT registered. Expected: the system responds the same way as TC1 from the user's perspective (no account-enumeration signal), but no email is actually sent.
- TC3 (edge, R1): Request a reset link twice in quick succession for the same email. Expected: the system's documented behavior for a second request (invalidate the first link, or allow both to be valid) is defined and tested explicitly, since the requirement as stated is silent on this.
- TC4 (positive, R2): Use the link at 29 minutes and 59 seconds after issuance. Expected: accepted.
- TC5 (negative, R2): Use the link at 30 minutes and 1 second after issuance. Expected: rejected with a clear "link expired" message, not a generic error.
- TC6 (edge, R2): Use the link at exactly 30 minutes and 0 seconds. Expected: the system's chosen boundary convention (inclusive or exclusive of the exact 30-minute mark) is explicitly defined and tested, per the same BVA discipline used for any numeric threshold.
- TC7 (positive, R3): Use a valid, unexpired link once. Expected: password reset succeeds, and the link is now marked used.
- TC8 (negative, R3): Attempt to reuse the SAME link a second time, immediately after a successful first use. Expected: rejected, even though the link has not expired by time (the 30-minute window may still be open); this distinguishes the expiry rule (R2) from the single-use rule (R3) as two INDEPENDENT invalidation conditions, and a test suite that only checks one after triggering the other would never catch a bug where a developer implemented just one of the two invalidation paths.
Trade-offs & pitfalls
The requirement as given is silent on two realistic conditions that a senior candidate should surface rather than silently assume: what happens to an EARLIER, still-unexpired link when a new reset is requested for the same email (TC3), and whether a partially-completed reset attempt (link used, but the new-password submission fails validation, e.g. a weak password) should still consume the link's single use. Deriving tests strictly from the literal requirement text risks missing these; the traceability matrix format helps here specifically because an empty or thin row for an ambiguous case is visibly incomplete, prompting the clarifying question before the ambiguity ships as an accidental behavior.
Discuss the trade-offs between exhaustive edge-case testing and targeted, risk-based edge-case testing. Cover cost, time, combinatorial explosion, diminishing returns, and contexts (such as safety-critical or regulated systems) where exhaustive testing may be required. Provide a practical framework or decision tree you would use to determine which cases to test exhaustively and which to sample or mitigate by other means (monitoring, canaries, runtime checks).
Sample Answer
Direct answer
Exhaustive testing is only tractable when the input or configuration space is genuinely small, or when the cost of a missed case is severe enough that no amount of test-execution cost is too high (safety-critical or regulated systems). Once independent parameters multiply, combinatorial growth outpaces any realistic test budget, so the actual skill is a risk-based framework that decides, per area, whether to test exhaustively, sample systematically (equivalence partitioning, boundary value analysis, pairwise), or shift the residual risk to a runtime compensating control (monitoring, canaries, invariant checks).
Structured elaboration
- Cost and combinatorial explosion: a full factorial test count is the product of every independent parameter's number of values, full factorial=∏i=1kvi, which grows multiplicatively, not additively, as parameters are added. Every additional test also carries an ongoing maintenance cost (it has to keep passing as the system evolves), which compounds the raw execution-time cost.
- Diminishing returns: after a first systematic pass with equivalence partitioning, boundary value analysis, and decision tables covering the known boundaries and known interaction points, each additional case deep in a large combinatorial space has a falling marginal chance of catching a genuinely NEW defect, since real bugs concentrate at boundaries and at low-order (2-way, 3-way) interactions far more often than they hide exclusively in high-order combinations. This is the practical motivation behind pairwise testing's popularity as a middle ground, though the exact fraction of interaction-triggered defects any specific coverage order catches is context-dependent and should not be quoted as a universal percentage.
- Contexts requiring exhaustive coverage: safety-critical domains (avionics, automotive, medical device software) frequently have externally imposed coverage requirements, up to full state-space or modified condition/decision coverage (a coverage criterion requiring every condition within a decision to be shown, independently, to affect that decision's outcome) for the highest-criticality code, where exhaustive verification is a regulatory floor, not a choice weighed against cost. Separately, a small, bounded, high-consequence space (for example, an 8-state finite state machine controlling a physical actuator) can be cheap enough to test exhaustively that there is no reason not to, independent of any regulation.
- A practical decision framework: (1) Is the space small enough that exhaustive testing costs less than the risk analysis itself would? Test it exhaustively and stop deliberating. (2) Is this path safety-critical or regulated? Exhaustive or the mandated coverage criterion applies regardless of size. (3) Otherwise, what is the blast radius of an undetected defect here? High blast radius (a revenue-critical path, a security boundary): apply a systematic technique at full rule or pair coverage. Lower blast radius: apply the same technique at a reduced sample, and rely on a compensating runtime control for the residual gap. (4) For everything not selected for pre-release testing, name the specific compensating control (an alert on an invariant violation, a bounded canary rollout percentage, a runtime assertion that fails loudly) rather than leaving the gap silently uncovered by anything.
Worked example
Consider a checkout page's cross-environment compatibility surface: operating system (4 values), browser (5), payment method (4), currency (3), region (6). Full factorial: 4×5×4×3×6=1440 test cases. Running 1,440 cases on every commit is not viable. This is not safety-critical or regulated, so step 2 of the framework does not apply, but it IS a revenue-critical path, so step 3 calls for a systematic technique at full coverage of at least 2-way interactions rather than dropping to an arbitrary small sample. An actual greedy pairwise-covering algorithm run against these five parameters produced a 30-test-case suite that covers every pair of values across every pair of parameters at least once, verified by recomputing the covered-pairs set from scratch and confirming zero pairs were missed, a 48x reduction from the full factorial (1440/30=48). The analytical lower bound for any pairwise suite here is the product of the two largest parameter domains, maxi=j(vi×vj)=6×5=30, meaning the actual generated suite achieved that lower bound exactly. The residual risk pairwise structurally cannot cover, a bug that only manifests under a specific 3-way or higher combination, is what step 4's compensating control exists for: an error-rate-by-browser production dashboard and a bounded canary rollout percentage catch what the pre-release suite intentionally does not attempt.
Trade-offs & pitfalls
Pairwise testing's 2-way guarantee is sometimes mistaken for "these are the only bugs that matter," when it is explicitly and only a 2-way interaction guarantee; a senior answer states the coverage gap for 3-way-and-higher interactions out loud rather than presenting pairwise as exhaustive-equivalent. Risk classification is also not a one-time decision: a code path that was low-risk at launch (an experimental, opt-in feature) can become high-risk once it defaults to on for all traffic, so the exhaustive-vs-targeted call needs to be revisited as usage changes, not fixed permanently at design time. Finally, this framework is specifically about WHICH cases to test before release, a distinct concern from prioritizing WHEN to run an existing test suite in a pipeline under time pressure, or triaging a test that fails intermittently, both of which are related but separate problems.
Unlock Full Question Bank
Get access to all 29 Test Case Design and Edge Case Analysis interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.