Articles
Writing Tests During a Coding Interview: Boundaries, Invariants, and Time Trade-Offs
Choose coding interview tests that expose boundary mistakes, check invariants, and fit the time available, with a worked Python example.
Writing tests during a coding interview is a problem of selection. You have limited time, an implementation that may contain a subtle mistake, and an interviewer who needs to understand why you trust the result. Five examples that all exercise the same path can provide less evidence than one carefully chosen duplicate or boundary case.
Choose each test to challenge a plausible wrong implementation. State the expected answer before running the code, and explain which assumption the case checks. You can rehearse that habit with Validate Unit-Test Coverage and Identify Missing Scenarios.
Evidence boundary: Official employer guidance below supports the importance of testing in those employers' stated interview contexts. It does not establish a universal scoring rubric. The examples, time budgets, and suggested answer patterns here are original practice material. No candidate report is used to claim that a particular question will appear.
Should you test before saying the solution is done?
Official guidance: Microsoft's technical interview preparation explicitly asks candidates to test solutions and consider boundaries, error conditions, and corner cases. Amazon's SDE II preparation identifies robust, well-tested code and edge-case checking as evaluation concerns. Neither source says that an arbitrary number of assertions guarantees success. See Microsoft technical interviewing and Amazon SDE II preparation.
Preparation advice: Agree on the contract before creating tests. A function that returns any matching index needs a different oracle from one that returns the first matching index. A scheduler that allows touching intervals needs different expected results from one that treats endpoints as conflicts.
Ask only the ambiguities that affect the implementation or its result. “Can the input be empty?” is useful if initialization indexes the first element. Asking about every imaginable malformed input can consume the time needed to solve the actual problem. When the prompt guarantees sorted integers, preserve that precondition unless the interviewer expands the task.
A helpful opening is: “I will confirm the return value for missing elements and duplicates, then use those cases to check the search.” That makes testing part of your reasoning from the start.
Turn the contract into a small set of discriminating cases
Consider this original exercise: given a sorted list of integers and a target, return the first index whose value is greater than or equal to the target. Return the list length if no such index exists. Do not change the input.
This is a lower-bound contract. Python's bisect documentation describes the corresponding partition for bisect_left: values before the returned position are smaller than the target, and values from that position onward are at least the target.
Before coding, distinguish an exact-match search from an insertion-position search. If the target is absent between two elements, returning -1 violates this contract. If several elements equal the target, returning the last one also violates it.
| Input and target | Expected index | Mistake this case challenges |
|---|---|---|
[], 4 | 0 | Reading an element before checking an empty range |
[5], 5 | 0 | Mishandling equality or a one-element range |
[2, 5], 1 | 0 | Losing the first possible position |
[2, 5], 8 | 2 | Returning the final element instead of the end position |
[2, 5], 3 | 1 | Treating an absent value as an error |
[2, 2, 5], 2 | 0 | Advancing past equal values |
This table is not a universal testing checklist. Its cases follow directly from one return-value contract and likely search mistakes. For another problem, build a different table instead of carrying over the same empty/singleton/large list mechanically.
Notice that the duplicate case is more informative than adding another ordinary successful lookup. It separates two algorithms that can agree on every example with distinct values.
Use a short executable harness when the environment permits it
Here is an original implementation and a compact assertion harness. It assumes sorted integer input; validating sortedness would add linear work and is outside this exercise's contract.
def lower_bound(values, target):
lo, hi = 0, len(values)
while lo < hi:
mid = lo + (hi - lo) // 2
if values[mid] < target:
lo = mid + 1
else:
hi = mid
return lo
cases = [
([], 4, 0),
([5], 5, 0),
([2, 5], 1, 0),
([2, 5], 8, 2),
([2, 5], 3, 1),
([2, 2, 5], 2, 0),
]
for values, target, expected in cases:
before = values.copy()
actual = lower_bound(values, target)
assert actual == expected, (values, target, actual, expected)
assert values == before
In a short coding exercise, a visible assertion can be enough to express the check. In a larger testing task, use the available framework for structured failure reporting and setup. Python's unittest documentation distinguishes test cases, fixtures, and assertions; you do not need to reproduce an entire framework to demonstrate those concepts.
Preparation advice: Do not spend the final minutes installing a library when a small built-in harness answers the question. Conversely, if the task is specifically to write unit tests in an existing project, use its established runner. The environment and requested deliverable determine the appropriate ceremony.
The harness should print or retain useful failing input. A message that says only “wrong” makes debugging slower. Showing the actual and expected index lets you connect the failure to the equality or end-position rule immediately.
Explain an invariant without confusing it with a test
For this implementation, maintain three facts: 0 <= lo <= hi <= n; every element before lo is smaller than the target; every element at or after hi is at least the target. The unresolved region is between those boundaries.
When the middle value is smaller, the first qualifying position must lie beyond it. Otherwise, that position can be the middle or somewhere earlier. Each update shrinks the unresolved region. At termination, lo == hi, so the returned position separates the two partitions.
That argument explains why the algorithm works under its assumptions. Individual tests show behavior for selected inputs. Both are useful, but neither should be described as the other.
Original counterexample: Suppose someone changes < target to <= target. The loop still terminates, and the index may remain within bounds. A property that checks only termination and range will miss the defect. The duplicate case returns 2 instead of 0, exposing the wrong equality boundary.
When you propose an invariant, ask whether an obviously wrong implementation could satisfy it. “The answer is a valid index” permits many wrong answers. A stronger result condition includes the actual partition, including the permitted end position n.
Build an independent oracle for practice
An oracle is how you determine the expected result. For small lists, a linear scan is simpler than binary search and makes a useful independent reference:
def reference(values, target):
return next(
(i for i, value in enumerate(values) if value >= target),
len(values),
)
Preparation advice: Compare the optimized solution against this reference on many small inputs during practice. Do not copy the same binary-search logic into the test; shared mistakes can make both implementations agree.
For this article, the implementation was checked against the linear oracle on every nondecreasing list of length zero through five drawn from {-1, 0, 1}, with targets from -2 through 2: 280 input-target combinations. The six explicit cases also passed. Changing the comparison to <= was caught by the equality cases. This is bounded verification, not proof for every integer list.
Small exhaustive checks are useful when the state space is cheap to enumerate. In the actual interview, their setup cost may exceed their value. Explain the idea if relevant, but prioritize the few cases most likely to reveal the current implementation's mistakes.
What if you cannot execute the code?
A manual trace can still be precise. Write the state after each meaningful update, rather than reading the code aloud and saying it “looks right.” Use the same contract and expected result you would use for an executable test.
For [2, 2, 5] and target 2, begin with lo = 0, hi = 3. The first middle index is 1; equality moves hi to 1. The next middle index is 0; equality moves hi to 0. The loop ends and returns 0.
Now trace the absent target 3 on [2, 5]. The first middle value is 5, so hi becomes 1. Then the middle value is 2, so lo becomes 1. The result is the insertion position, not an exact match.
Preparation advice: Say what was actually checked: “I traced the equality boundary and the missing interior value; I have not executed the code in this editor.” That is stronger than implying a manual walkthrough is a passing test run. If execution becomes available, run the selected cases before adding new ones.
Allocate testing time by risk, not by a fixed percentage
The following budgets are practice suggestions, not employer rules. Adapt them to the interviewer's instructions and the remaining problem.
If only two minutes remain, run or trace the provided example, the highest-risk boundary, and a case that challenges your latest code change. Confirm the return format and state your complexity. Resist adding a generic framework or a large random generator at that point.
If eight minutes remain, add cases that distinguish duplicates, missing answers, and extreme positions. Check side effects required by the contract. If an independent oracle is quick to write, compare a few small cases. Leave time to interpret failures instead of starting a test campaign you cannot explain.
For a graph problem, the highest-risk case may be a disconnected component. For a mutable data structure, it may be a sequence of operations rather than one call. For interval merging, it may be two touching endpoints. The principle is to spend time on a different failure mechanism, not simply a larger input.
Large-input testing has a different role. A tiny correctness suite cannot establish acceptable runtime at scale. Explain the asymptotic bound, identify the input shape that stresses it, and distinguish an actual measurement from a complexity argument. Avoid claiming that one fast run proves an algorithm efficient.
Recover well when a test fails
A failing case is useful evidence. First confirm that the expected result follows from the agreed contract. Then identify the smallest input that still fails. Follow the state changes until you find the first incorrect decision, fix that decision, and rerun the original failure plus nearby cases.
In the lower-bound example, changing the equality comparison should trigger a rerun of both duplicate and singleton-equality cases. Also retain a missing-value case so the fix does not accidentally turn insertion search into exact-match search.
Suggested explanation: “This test shows I advanced past equal values. The contract requires the first qualifying position, so equality must keep the middle position in consideration. I will rerun the duplicate case and the absent-target case after changing that branch.”
Do not silently change the expected answer to match the program. If the contract was ambiguous, state the newly agreed interpretation and update both code and tests openly. If the code is still incomplete when time ends, identify the unverified behavior precisely rather than claiming the suite establishes correctness.