Create your own
Lesson illustration

Designing Boundary, Invalid, and Adversarial Test Cases

Hello. In the previous lesson, you translated a settled algorithm into Java and performed a quick trace to catch syntax, scope, and ordering mistakes. That is necessary, but it is not sufficient: a solution can look clean and succeed on one ordinary example while failing at the first or last index, on duplicate values, or when an input violates an unstated assumption.

This lesson makes testing systematic. You will learn to construct boundary, invalid, and adversarial test cases for an algorithm, state the expected result precisely, and explain your choices during a coding interview. This is the final testing-focused skill in the Coding Interview Foundations module; the next lesson will focus on tracing an algorithm’s internal state to locate invariant and off-by-one failures.


Start with the contract, not the test data

A test case is more than an input. It has four parts:

  1. Input — the values given to the method.
  2. Expected result — output, exception, mutation, or other observable behavior.
  3. Purpose — the rule, boundary, branch, or assumption being checked.
  4. Failure hypothesis — the kind of bug the case could expose.

The expected result must come from the problem’s contract. Before proposing “edge cases,” separate these three ideas:

CategoryMeaningExample for an array method
Possible inputA value Java can receive at runtime.A non-null array, an empty array, or null.
Valid inputAn input that meets the stated problem constraints.An array with at least two elements when the prompt guarantees that.
Invalid inputA possible input that violates a stated requirement.null when the contract requires a non-null array.

This distinction prevents a common interview mistake: inventing behavior that was never requested.

Suppose the interviewer gives the standard Two Sum contract:

Given a non-null integer array containing at least two elements, return indices of two distinct elements whose values sum to target. Exactly one solution exists.

Under this contract:

  • nums = {3, 3}, target = 6 is valid.
  • nums = {3}, target = 6 is invalid, because the array is too short.
  • nums = null is invalid, because the input must be non-null.
  • nums = {1, 2, 3}, target = 99 is invalid under the “exactly one solution exists” guarantee, even though it is an ordinary Java array.

You should not silently decide that every invalid input returns an empty array. Instead, clarify:

“May I assume the stated constraints hold? If not, should invalid input throw an exception, return an empty result, or use another error signal?”

That question is not a detour. It defines what “correct” means.

How to Solve ANY Coding Interview Question in 6 Steps

Watch How to Solve ANY Coding Interview Question in 6 Steps by Anthony D. Mays. It shows why examples with expected outputs belong before and after implementation, rather than being an afterthought.

Watch examples first to see how sample inputs and outputs expose misunderstandings before coding. Then watch testing the code for a concise interview-oriented testing pass, including bad input, null, zero, and boundary conditions.

A useful habit is to say your assumption aloud before writing tests:

“I’ll first test valid inputs under the stated guarantee. Then I’ll mention how I would handle null, too-short arrays, and no-solution cases if the API needs to support them.”

This keeps the interviewer involved and makes your reasoning reviewable.


Boundary tests: test the values where rules change

A boundary is a limit of a valid or invalid region. Boundary testing targets the exact limit and the closest values on each side, because programmers frequently make mistakes such as using the wrong comparison operator or stopping a loop one iteration too early.

For a numeric requirement such as “accept values from 1 through 100, inclusive,” a strong compact boundary set is:

Position relative to rangeTest valueExpected classification
Just below minimum0Invalid
Minimum1Valid
Just inside minimum2Valid
Just inside maximum99Valid
Maximum100Valid
Just above maximum101Invalid
A number line for an input range from 1 through 100: 1, 2, 99, and 100 are valid values, while 0 and 101 fall immediately outside the permitted range.

For a lower bound and upper bound , this pattern is commonly written as:

The set may overlap for tiny ranges. If the valid input is exactly one value, there is no need to create redundant cases merely to fill six slots. Test the actual rule.

Boundary testing applies beyond number validation. In algorithmic problems, the important boundaries often include:

  • Collection size: empty, one element, two elements, and a larger ordinary case.
  • Indices: first index, last valid index, and a point just beyond the last valid index.
  • Loop transitions: zero loop iterations, exactly one iteration, and the final iteration.
  • Window length: window smaller than required, exactly required, and one larger.
  • Search ranges: target below the smallest element, equal to the smallest, equal to the largest, and above the largest.
  • String length: empty string, one character, and lengths directly around a stated limit.

The important question is not “What are some unusual values?” It is:

“At what input does the algorithm or specification change behavior?”

For the Two Sum hash-map solution from the previous lesson, array length two is a meaningful boundary. It is the shortest valid array that can contain two distinct indices.

nums = new int[] {4, 5};
target = 9;
// Expected: new int[] {0, 1}

This test checks that the algorithm does not assume a third element exists and that it can find a pair on its final possible iteration.

Boundary Value Analysis According to the ISTQB® Foundation ...

Read this ISTQB paper for a rigorous but practical account of boundary-value analysis. Its login-length example turns the familiar “minimum, maximum, just inside, just outside” idea into a repeatable procedure.

Begin in Section 1, “The technique,” with why boundaries matter. Then read Section 3, “BVA step by step,” including the login example and its two tables; focus on how the valid range and the too-short and too-long ranges become separate equivalence partitions. In that example, follow the partitioning step before reviewing the selected test values. Continue through Section 5, “Defect types addressed by the BVA.” Pay particular attention to the comparison-operator example: it demonstrates why testing only a boundary and its outside neighbor can miss certain defects. Finish with the first two paragraphs of Section 7, “Further practical considerations,” especially possible versus invalid inputs.

Equivalence partitions keep testing efficient

An equivalence partition is a group of inputs expected to behave the same way. Rather than testing every valid login length from 6 through 15, you can choose a representative interior value, then spend extra attention at the region boundaries.

For a rule “login length must be from 6 through 15,” the partitions are:

  • too short: lengths from 0 through 5;
  • valid: lengths from 6 through 15;
  • too long: lengths 16 and above.

A basic boundary suite includes lengths 5, 6, 15, and 16. A stronger suite also includes nearby interior values, such as 4, 7, 14, and 17. The stronger version catches errors that accidentally accept only one exact boundary value rather than the full intended range.

In interviews, you rarely need to announce the formal testing terminology. A plain explanation is often clearer:

“I want values just below, at, and just above each point where behavior changes, because that is where an incorrect loop condition or comparison is most likely to appear.”


Invalid tests: verify the policy, not your guess

An invalid test case checks the method’s behavior when the caller violates a specified precondition. It is only useful when an expected response has been defined.

Consider this variation of an API contract:

public int[] twoSum(int[] nums, int target)

Suppose the API explicitly says:

  • nums must not be null;
  • nums must contain at least two elements;
  • a pair may or may not exist;
  • return an empty array when no pair exists;
  • throw IllegalArgumentException for invalid nums.

Now these invalid tests have unambiguous expected results:

CaseInputExpected resultWhat it verifies
Null referencenums = nullIllegalArgumentExceptionValidation happens before nums.length is accessed.
Too shortnums = {8}IllegalArgumentExceptionThe distinct-index requirement is enforced.
Emptynums = {}IllegalArgumentExceptionThe same size rule works at the lower boundary.

And this is not an invalid-input test:

CaseInputExpected resultWhy
No valid pairnums = {1, 2, 3}, target = 99Empty arrayIt meets the input-shape rules; it simply has no answer.

This distinction matters because invalid inputs and valid “no result” inputs normally deserve different behavior. Conflating them creates confusing APIs and weak interview reasoning.

In a coding interview, avoid spending most of the session implementing defensive behavior unless it is part of the prompt. State the decision, handle it if required, then return to the central algorithm.


Adversarial tests: target the algorithm’s likely mistakes

A boundary test targets a changing limit in the specification. An adversarial test targets a plausible weakness in the implementation or a tempting but flawed alternative approach.

Adversarial does not mean random, huge, or malicious by default. It means deliberately designed to falsify an assumption.

For the one-pass Two Sum solution, recall the essential state order:

if (indexByValue.containsKey(complement)) {
    return new int[] {indexByValue.get(complement), i};
}

indexByValue.put(nums[i], i);

The map lookup must happen before the current number is added. Otherwise, an element can match itself.

Here is a focused test suite for that algorithm.

CategoryInputExpected resultBug or assumption targeted
Ordinary valid case{2, 7, 11, 15}, target 9{0, 1}Confirms basic lookup and returned indices.
Minimum valid size{4, 5}, target 9{0, 1}Checks the smallest permitted input size.
Pair spans endpoints{2, 99, 7}, target 9{0, 2}Detects failure to process the final element.
Duplicate values{3, 3}, target 6{0, 1}Confirms equal values can use two distinct indices.
Zero values{0, 4, 0}, target 0{0, 2}Tests zero without allowing self-matching.
Negative values{-4, 10, 6}, target 2{0, 2}Rejects an unjustified non-negative-values assumption.
No solution, if permitted{1, 2, 3}, target 99Empty arrayChecks the fallback path.
Invalid size, if defined{3}, target 6Exception or other specified responseExposes a self-match error and verifies validation.
Null, if definednull, target 0Exception or other specified responseVerifies the null-input policy.

Notice how the duplicate case differs from the one-element case:

  • {3, 3} is valid because there are two separate elements.
  • {3} must not produce {0, 0}, because the problem requires distinct indices.

That one distinction catches a surprisingly common defect: inserting the current value before searching for its complement.

Derive adversarial cases from code structure

Once you have an algorithm, inspect its assumptions systematically rather than relying on inspiration.

1. Find every branch and make it take both directions

For Two Sum, containsKey(complement) must be both false and true:

  • false on the first iteration of an ordinary case;
  • true when the matching second value is reached;
  • false for every iteration of a no-solution case, if no-solution inputs are allowed.

2. Test the first and last use of every index

An indexed loop has two especially risky moments:

  • the first iteration, when initialization errors show up;
  • the final iteration, when an off-by-one condition can skip a valid answer or access outside the array.

The endpoint-pair test {2, 99, 7} with target 9 checks both.

3. Attack the invariant

An invariant is a statement that should remain true while the algorithm runs. For the hash-map approach, the invariant is:

Before processing index i, the map contains values from earlier indices and the index at which each stored value was seen.

The duplicate test attacks this invariant. If the code stores the current value too early, the map no longer represents only earlier positions at the moment of lookup.

4. Challenge data assumptions

Ask whether the code wrongly assumes:

  • values are positive;
  • values are unique;
  • the answer uses adjacent elements;
  • input is already sorted;
  • input can be mutated;
  • there is exactly one answer;
  • arithmetic cannot overflow.

For example, a sorting-based Two Sum implementation may find values that sum correctly but return indices from the sorted array rather than the original array. A good adversarial test puts the answer in non-adjacent positions and checks the returned original indices, not merely the values.

5. Challenge resource assumptions

For an algorithm intended for large inputs, add a large conceptual case even if you do not manually write thousands of values in an interview. State what it is meant to demonstrate:

“For a large array with no matching pair, this hash-map approach should complete in linear time and use space proportional to the number of elements. A nested-loop alternative would become impractical.”

For numeric algorithms, clarify arithmetic semantics. Java int arithmetic can overflow. If the prompt permits arbitrary int values but expects mathematical sums, use long for intermediate calculations or ask whether wraparound semantics are intended. A near-limit test should have an explicit expected result only after that decision is made.


Turn the method into an interview-ready test pass

After coding, do not list ten arbitrary cases. Give a compact test narrative that covers categories and explains their purpose.

For the Two Sum method with the “exactly one solution” guarantee, you might say:

“I’ll trace a normal case such as {2, 7, 11, 15} with target 9. Then I’ll use the minimum valid length {4, 5} to test the array boundary. I’ll test duplicates with {3, 3} and target 6, which confirms that two equal values at different indices work and that the algorithm does not reuse one index. Finally, I’ll test a pair whose second value is at the last position, such as {2, 99, 7} with target 9, to check the loop’s upper boundary.”

This is concise, but it demonstrates that your tests are intentional.

A reusable 60-second checklist

Before you call a coding solution complete, perform this pass:

  1. Contract: What inputs are valid? What must happen for invalid inputs and no-result inputs?
  2. Normal representative: Do I have one ordinary input with a known expected output?
  3. Size boundaries: What happens with empty, one-element, minimum valid, and final-index cases?
  4. Value boundaries: Are there stated minimums, maximums, thresholds, or transitions?
  5. Special values: Do zero, negatives, duplicates, equal values, or repeated characters change the logic?
  6. Branches: Does at least one test execute each meaningful conditional outcome?
  7. Algorithm assumptions: What shortcut, ordering rule, invariant, or mutation could be wrong?
  8. Resources: Could large input size, recursion depth, or integer arithmetic change correctness or feasibility?

Keep the checklist in your notes until it becomes automatic. It is more reliable than trying to “think of edge cases” from scratch under interview pressure.


Key takeaways

Strong test cases are derived from the contract and the code’s assumptions:

  • A complete test case includes an input, expected result, purpose, and likely defect it can reveal.
  • Boundary tests examine exact limits and nearby values where behavior changes.
  • Invalid tests require a specified policy; do not invent return behavior for inputs outside an interview prompt’s contract.
  • Adversarial tests deliberately challenge likely bugs, such as duplicate handling, self-matching, skipped final indices, hidden sorting, and unjustified value assumptions.
  • For Java array algorithms, always consider empty input, minimum valid length, first index, and last valid index.
  • Explain a small, targeted test suite aloud. That is stronger than naming many disconnected edge cases.

Next, you will trace Java algorithms manually, using variable state and invariants to identify exactly where an off-by-one error or state violation occurs.

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

Sign up