Create your own
Lesson illustration

Design Boundary and Adversarial Test Cases First

Good to see you again. In the previous lesson, you learned to use invariants to state what must remain true while an algorithm runs. Testing is the complementary habit: instead of assuming the invariant and branch logic are right, you deliberately choose inputs likely to break them.

This is the final lesson in the interview-reasoning toolkit. The goal is not to test every imaginable input during a coding interview. It is to construct a small, high-yield set of boundary and adversarial cases before submitting: cases that challenge initialization, loop exits, pointer movement, duplicate handling, numeric limits, and assumptions in the prompt.


Testing starts with the contract, not with random examples

A test case has three parts:

  1. a concrete input;
  2. an expected result or property; and
  3. a reason that input is risky.

For a function such as:

static boolean hasPairWithSum(int[] nums, int target)

you cannot choose meaningful tests until its contract is settled. Ask questions that change what “correct” means:

  • Is nums sorted, or must the method work on any order?
  • Can it be empty?
  • Are duplicate values allowed?
  • Must the two values come from distinct indices?
  • Can values be negative?
  • What are the numeric constraints?
  • Is a pair guaranteed to exist?
  • Are null inputs excluded by the platform?

In an interview, input constraints are usually part of the contract. If the interviewer says the array is non-null, sorted, and has valid values, do not spend valuable implementation time inventing error-handling behavior. Still, test the smallest legal arrays and the most difficult legal values.

Watch this short segment of How To Pass Coding Interviews Like the Top 1% by Tech With Tim. It connects clarification questions to test-case construction, which is exactly the habit to develop before coding.

How To Pass Coding Interviews Like the Top 1%

Watch “How To Pass Coding Interviews Like the Top 1%” by Tech With Tim for a practical interview routine: clarify the contract first, then create your own cases rather than relying on examples.

Watch clarifying inputs for the questions that determine which edge cases matter. Then watch creating cases, where the speaker recommends tracing self-created empty, negative, and duplicate-value examples before proceeding.

A useful interview sentence is:

“Before I implement, I’ll confirm whether duplicates and negative values are allowed, whether a result is guaranteed, and whether the input is already sorted. I’ll test the smallest valid inputs, duplicates, and a no-solution case after coding.”

That communicates deliberate reasoning without delaying the solution.


Equivalence classes and boundaries

Most input domains can be divided into equivalence classes: groups of inputs that should make the program behave in the same essential way. Test one representative from each meaningful group, then focus extra attention where behavior changes.

For a valid integer range from through , the broad classes are:

ClassRepresentativeExpected behavior
Below valid range0Reject or handle according to the specification
Valid range50Accept
Above valid range101Reject or handle according to the specification

The important defect-prone points are the transitions between classes: 0/1 and 100/101. These are boundaries.

A valid integer range from 1 through 100, showing the two valid endpoints and values immediately inside and outside each boundary; these nearby values expose inclusive-versus-exclusive mistakes.

The same idea applies to algorithmic input sizes. If an array has length , then common structural boundaries are:

  • : the empty structure;
  • : no pair of distinct indices exists;
  • : the first size at which a pair can exist;
  • a larger input: enough elements to exercise repeated loop iterations.

For indexes, the boundaries are usually the first valid index, 0, and the last valid index, n - 1. Bugs concentrate there because code often uses conditions such as i < n, left < right, or right >= 0.

Read the introductory material and the “Types of BVA” subsection in Software Testing – Boundary Value Analysis from GeeksforGeeks. Translate its software-testing examples into interview terms: identify a valid region, test its endpoints, and—when the problem permits invalid inputs—test immediately outside them.

Software Testing - Boundary Value Analysis

Read this GeeksforGeeks overview to formalize a practical intuition: tests near a change in behavior are more valuable than arbitrary “normal” inputs.

In the introduction, read the core idea and the list under “For Each Input Variable, BVA Tests.” Then, under “Types of BVA,” compare “Normal BVA” with Robust BVA. For interview problems, use normal boundary tests for guaranteed-valid inputs; use robust tests only when the contract requires validation or leaves invalid input behavior open.

Do not confuse “boundary” with “invalid”

A boundary test need not be invalid.

For a valid range , the values 1 and 100 are legal but essential tests. They catch code that accidentally uses > rather than >=, or < rather than <=.

Similarly, for an array problem whose constraints guarantee , an empty array is not a legal test. You may mention it as a clarification question, but spend your actual test time on legal corner cases such as a one-element array, maximum-size considerations, duplicates, or extreme numeric values.


From an invariant to a test plan

The previous lesson gave you a powerful source of tests: every part of an invariant suggests a way implementation can fail.

Suppose a loop has this shape:

int left = 0;
int right = nums.length - 1;

while (left < right) {
    // inspect nums[left] and nums[right]
    // move left, move right, or return
}

The loop has critical state transitions:

Risk in the implementationTest that targets it
Initial pointers are invalidEmpty input, if allowed
Loop should not runOne-element input
Loop runs exactly onceTwo-element input
left moves repeatedlyA case whose solution requires advancing left
right moves repeatedlyA case whose solution requires decreasing right
Equality branch is wrongA pair exists at the current endpoints
Exit condition is wrongNo valid answer exists
Distinct-index requirement is violatedOne matching value versus two equal matching values

This is better than memorizing a vague checklist. Read the code, identify every branch and state boundary, and create an input that forces that behavior.

A compact rule:

Each meaningful branch deserves a test; each loop boundary deserves a test; each stated assumption deserves a challenge.

Example: tests for a sorted two-sum decision function

Assume the contract is:

  • nums is non-null and sorted in nondecreasing order;
  • duplicates and negative values are allowed;
  • two distinct indices are required;
  • return whether any pair sums to target.

Before writing or submitting the two-pointer solution, construct this test sheet:

PurposeInputTargetExpected
Empty boundary, if legal[]5false
One element cannot form a pair[5]10false
Smallest successful input[2, 7]9true
Smallest unsuccessful input[2, 7]8false
Solution at endpoints[1, 3, 4, 9]10true
Must move left[1, 2, 4, 7, 11]15true
Must move right[1, 4, 6, 8, 10]7true
Duplicate values form the pair[5, 5]10true
One copy is insufficient[5]10false
Negative values[-8, -3, 1, 6, 10]-2true
No solution after search space closes[1, 2, 4, 8]10false

Notice that [5, 5] and [5] test a semantic distinction, not merely two different sizes. An incorrect solution that allows one element to pair with itself can pass ordinary examples yet fail the one-copy case.

The “move left” and “move right” rows test the two elimination arguments from the invariant:

  • if the current sum is too small, the left endpoint must be discarded;
  • if it is too large, the right endpoint must be discarded.

If a solution works only when the answer begins at the endpoints, it has not really tested pointer movement.


Adversarial cases target hidden assumptions

A boundary case lies at a transition in the valid domain or control flow. An adversarial case is a legal input selected specifically to violate a tempting but unjustified assumption in your code.

Common hidden assumptions in DSA solutions include:

Hidden assumptionAdversarial input shape
“Values are unique.”Repeated values, including all values equal
“Numbers are positive.”Negative values, zeros, or mixed signs
“The answer exists.”A valid no-solution input
“The answer is in the middle.”Answer at the first or last position
“The input is unsorted enough to be interesting.”Already sorted, reverse sorted, or all equal
“An int calculation is safe.”Values near Integer.MAX_VALUE or Integer.MIN_VALUE
“The loop runs several times.”Empty, singleton, or two-element structures
“Recursion will be shallow.”Deepest valid tree, graph chain, or maximum-length input

Numeric overflow is an adversarial test

Consider a two-sum implementation that performs addition in int:

int sum = nums[left] + nums[right];

This may overflow before the comparison occurs. The following is a legal adversarial test if the input range permits arbitrary int values:

int[] nums = {Integer.MAX_VALUE, Integer.MAX_VALUE};
int target = -2;

Mathematically, the two values sum to a number larger than an int; they do not sum to -2. But Java int arithmetic wraps around, and an unsafe implementation may incorrectly return true.

Use widened arithmetic when the operation can exceed int range:

long sum = (long) nums[left] + nums[right];

The cast must occur before addition. This test directly connects to your earlier comparator lesson: Java integer arithmetic is bounded, so arithmetic shortcuts must be justified against the problem’s constraints.

Equality is often a branch boundary

Whenever code chooses among <, ==, and >, equality deserves its own case.

Examples:

  • Binary search: target equal to the midpoint; duplicate values if the required output is a first or last occurrence.
  • Interval merging: intervals that touch at an endpoint, such as [1, 3] and [3, 5]. Whether they merge depends on the definition of overlap.
  • Sliding windows: a window sum exactly equal to the threshold.
  • Heap ordering: two records with equal priority.
  • Graph traversal: multiple neighbors that could reach a node at the same shortest distance.

An equality case is adversarial because it exposes code that accidentally treats equality as belonging to the wrong branch.


Test the result’s properties, not only one exact output

Some interview problems permit multiple correct outputs. In those cases, an exact expected array or ordering may be the wrong test oracle.

For example, an in-place “remove all occurrences of value” function usually returns a new logical length. The elements beyond that length are irrelevant. A strong test checks the required properties:

static void checkRemoveResult(int[] nums, int newLength, int value) {
    for (int i = 0; i < newLength; i++) {
        if (nums[i] == value) {
            throw new AssertionError("Removed value remains in active prefix");
        }
    }
}

Suppose the method is:

static int removeElement(int[] nums, int value)

High-yield tests include:

CaseInputvalueExpected logical length
Empty input[]30
No values removed[1, 2, 4]33
Every value removed[3, 3, 3]30
First element removed[3, 1, 2]32
Last element removed[1, 2, 3]32
Alternating removals[3, 1, 3, 2, 3]32

For a partitioning algorithm, test the postcondition on each completed region. For a graph algorithm, test reachability or shortest-path distance. For a tree algorithm, test both the returned value and whether the tree must remain unchanged or may be mutated. The expected result must match the problem’s actual contract.


A 60-second pre-submission routine

Once the main implementation compiles, pause before submitting. Do this in order:

  1. Restate the contract. Check whether your code matches requirements on mutation, ordering, duplicates, nullability, and no-solution behavior.
  2. Choose size boundaries. Mentally run the empty case if legal, then sizes one and two.
  3. Force every branch. Locate each if, else if, and loop exit. Name one input that reaches it.
  4. Attack assumptions. Consider duplicates, negatives, zeros, endpoint answers, and extreme integers.
  5. Trace one nontrivial case. Write pointer, index, map, stack, or heap state after each significant update.
  6. Check output properties. Verify not only the returned value, but also ordering, mutation, bounds, and distinct-index requirements.
  7. Recheck complexity hazards. For maximum constraints, ask whether recursion depth, nested loops, object allocation, or an int calculation can fail.

For interview code, a lightweight Java harness helps keep the reasoning concrete:

static void check(boolean actual, boolean expected, String name) {
    if (actual != expected) {
        throw new AssertionError(
            name + ": expected " + expected + ", got " + actual
        );
    }
}

You do not need to write a full test suite on the shared editor. A few short calls and a spoken trace are usually enough:

check(hasPairWithSum(new int[] {5, 5}, 10), true, "duplicate pair");
check(hasPairWithSum(new int[] {5}, 10), false, "distinct indices");
check(hasPairWithSum(new int[] {1, 2, 4, 8}, 10), false, "no pair");

The key is that each case has a purpose. “I tested several examples” is weak. “I tested the singleton boundary, duplicate-value semantics, pointer movement in both directions, and a no-solution exit” is a correctness argument.


Key takeaways

Boundary testing concentrates on points where behavior changes: empty versus nonempty, one element versus two, first and last indexes, inclusive limits, and equality branches.

Adversarial testing challenges assumptions that ordinary examples hide: duplicates, negatives, no solution, endpoint answers, overflow, degenerate structure, and worst-case shape. Use only cases that are valid under the stated contract unless input validation is itself required.

The most reliable way to create tests quickly is to derive them from the implementation:

  • test initialization;
  • force every branch;
  • test the first and final loop iterations;
  • test the conditions used in the invariant; and
  • verify the actual output properties.

You now have the core interview-reasoning toolkit: infer feasible complexity, select structures, control recursion, reason about pointer movement, write safe comparators, state invariants, and test adversarially before submission. The next module begins the high-yield array and string patterns with sorted pair and triplet problems using opposing two pointers.

Can't find a good explanation? Sign up and we'll make it for you

Sign up