Create your own
Lesson illustration

Constructing Edge-Case Tests for Array and String Algorithms

Good to see you again. In the previous lesson, you learned that a sorting-based solution needs both a correct numeric comparator and an honest complexity statement. Correct code also needs a deliberate testing habit: a solution can look convincing on a normal example while failing at an empty input, a boundary index, or a duplicate in the wrong position.

This closes the foundations module. You will learn a compact method for constructing edge-case tests for array and string algorithms, stating their expected outputs, and using them to expose assumptions in your code before an interviewer does. Plan for about 35–40 minutes, including the short video and reading.


Edge cases are contract tests, not random “weird inputs”

An edge case is an input near a meaningful limit or transition in the problem’s rules. It is where the algorithm changes behavior:

  • a collection changes from empty to non-empty;
  • an index moves from valid to out of bounds;
  • a count changes from zero to one;
  • a duplicate changes an answer from false to true;
  • a pointer reaches the first or last element.

The important phrase is in the problem’s rules. Before writing tests, establish the input contract.

Suppose a prompt says:

Given an array of integers, return true if any value appears at least twice.

You should clarify:

  • Can the array be empty?
  • Can numbers be negative or zero?
  • Are duplicates allowed?
  • Is the input guaranteed to be an array of integers?
  • Do we return a Boolean only?

If the interviewer says “nums is a valid integer array,” then null, "hello", and objects are not primary algorithm tests. They could matter in production API code, but adding defensive validation in an interview may distract from the required algorithm.

In JavaScript, this distinction prevents a common mistake:

if (!str) {
  return false;
}

This treats "", null, and undefined identically because they are all falsy. That is correct only if the contract intentionally gives them the same meaning. Usually, an empty string is a valid input with a defined output, while null is either disallowed or must be handled separately.

Watch this short segment from How To Pass Coding Interviews Like the Top 1% by Tech With Tim. It models the practical sequence: clarify constraints first, then create your own tests rather than relying only on examples supplied in the prompt.

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 concise interview-oriented routine for clarifying constraints and generating test cases.

First watch clarifying constraints. Focus on how questions about empty inputs, numeric ranges, and whether an answer is guaranteed determine which cases you actually need to handle. Then watch custom test cases, where the speaker turns empty inputs, negative values, and duplicates into concrete test scenarios before coding.


A five-part method for constructing tests

Avoid memorizing a giant list of cases and trying to use all of them. Instead, derive a few high-value tests from the prompt and your proposed approach.

1. Write the contract in one sentence

State input, output, and constraints precisely.

For example:

containsDuplicate(nums) accepts an integer array and returns whether any value occurs at least twice.

This gives you a way to identify ambiguity. If empty arrays are allowed, then [] needs an expected answer. If numbers can be negative, a solution must not accidentally assume positivity.

2. Locate the decision boundaries

Look for places where behavior changes:

Boundary typeTypical array or string test
Collection sizeEmpty, one element, two elements
PositionFirst element, last element, adjacent positions
ValueZero, negative value, minimum or maximum allowed value
FrequencyNo duplicates, one duplicate pair, all duplicates
ConditionExactly at a threshold, just below it, just above it
String structureEmpty string, one character, repeated characters, separators

A two-element input is particularly valuable. It is the smallest input that exercises a relationship between two positions: comparison, swapping, pair matching, or a loop that starts at index 1.

3. Choose inputs that challenge an assumption

Every test should have a reason. Ask:

What does my solution assume, and what smallest legal input challenges that assumption?

For a Set-based duplicate check, the key assumption is that a repeated value may occur anywhere, not only beside its earlier occurrence. So [4, 9, 4] is stronger than [4, 4]: it catches an implementation that checks only adjacent values without sorting.

For a scan that accesses nums[i + 1], a one-element array challenges the assumption that a neighbor exists. For a string parser using split(" "), consecutive spaces challenge the assumption that splitting produces only meaningful words.

4. Specify the expected output before running code

A test is not merely an input. It has three components:

InputExpected outputWhat it checks
[]falseNo accidental access to a first element
[8]falseSingle item cannot be duplicated
[2, 5, 9]falseNormal no-duplicate case
[2, 5, 2]trueDuplicate separated by another value
[0, -3, 0]trueZero and negative values are handled normally

Notice that each expected output can be determined from the specification, without trusting the implementation. That makes the test useful.

5. Dry-run the risky tests after coding

Do not only print the final result. Trace the variables that express your algorithm’s state:

  • loop index and array access;
  • Set or Map contents;
  • left and right boundaries;
  • counts;
  • current best answer.

A bug becomes easier to locate when you can say, “At this iteration, the count should have become zero, but the map entry remained,” rather than only, “The output is wrong.”


Boundary value analysis

Many off-by-one bugs come from testing at a boundary but not testing just around it. Boundary value analysis uses three nearby values:

  1. Just below the boundary.
  2. Exactly at the boundary.
  3. Just above the boundary.

For an inclusive allowed numeric range from through , the informative tests include 0, 1, 2, 99, 100, and 101.

This number line shows a valid inclusive range from 1 to 100, with values immediately below and above the range marked invalid. It illustrates why tests should target both the boundary itself and its immediate neighbors.

Whether 0 and 101 should be passed to the function depends on the contract:

  • If the problem guarantees values in the range, 1, 2, 99, and 100 test valid boundary behavior.
  • If the function must validate input, 0 and 101 test rejection or error handling too.

The same idea applies to an array-length parameter. If a problem uses a window size and permits , important cases are:

  • , where no element may be included;
  • , the first non-empty window;
  • , where the whole array may be used.

An algorithm often fails not in the middle of a range, but at a comparison such as <= versus <, or at an array access just past the final valid index.


Build tests from the approach, not only from the input type

Generic categories are useful, but the best tests target the invariant or state transition in your chosen solution.

Consider a function:

function containsDuplicate(nums) {
  const seen = new Set();

  for (const num of nums) {
    if (seen.has(num)) {
      return true;
    }
    seen.add(num);
  }

  return false;
}

The important invariant is:

Before processing the current value, seen contains exactly the values from earlier positions.

That invariant suggests this compact test set:

containsDuplicate([]);           // false
containsDuplicate([7]);          // false
containsDuplicate([1, 2, 3]);    // false
containsDuplicate([1, 2, 1]);    // true
containsDuplicate([5, 5, 5]);    // true
containsDuplicate([0, -1, 0]);   // true

These cases are not six arbitrary examples:

  • [] checks that the loop and final return work when no state changes occur.
  • [7] checks the smallest valid non-empty collection.
  • [1, 2, 3] ensures the algorithm does not report a duplicate merely because it has scanned multiple values.
  • [1, 2, 1] checks a non-adjacent duplicate.
  • [5, 5, 5] checks that the first repeated occurrence is enough to return true.
  • [0, -1, 0] checks value assumptions, especially accidental truthiness logic.

Now compare a different specification:

Return the index of the first character that appears exactly once, or -1 if none exists.

A good test set must distinguish any unique character from the first unique character:

firstUniqueIndex("");        // -1
firstUniqueIndex("z");       // 0
firstUniqueIndex("aabbcc");  // -1
firstUniqueIndex("aabbc");   // 4
firstUniqueIndex("abac");    // 1

The final case, "abac", is especially useful. Both b and c are unique, but the required answer is the earlier one, b at index 1. A solution that simply finds the last unique character would fail this case.

This is the larger principle:

A high-value edge case separates your correct algorithm from a plausible but wrong algorithm.


Array and string test categories to keep ready

Read the following focused checklist. Its purpose is not to make you recite thirty cases; use it to recognize categories, then select the few that stress the exact solution you are writing.

Coding Interview Edge-Case Checklist: 30 Edge Cases You Should Say Out Loud | Beyz AI

Read Coding Interview Edge-Case Checklist from Beyz AI for a categorized reference of array and string edge cases, followed by a practical method for selecting only the tests relevant to your approach.

In the opening checklist, scan the categories “Boundaries & sizes,” “Value ranges & numeric traps,” “Duplicates & frequency logic,” “Ordering & adversarial layouts,” and “Strings & parsing assumptions.” Read the relevant checklist range, asking which category could invalidate an assumption in your current solution. Then, in the section “Using the checklist in real time,” read the selection method. Focus on its rule to connect each chosen test to an invariant or assumption, rather than listing cases mechanically.

For the problems in this sprint, the following compact mental checklist is enough.

For arrays

Check whether the prompt and your approach require tests for:

  • Size: [], one element, two elements.
  • Location: target or duplicate at the first or final index.
  • Duplicates: none, one pair, all values equal, duplicates far apart.
  • Values: zero, negative values, extremes allowed by constraints.
  • Order: already sorted, reverse sorted, or unsorted input when ordering matters.
  • Thresholds: a parameter equal to zero, one, array length, or just beyond a permitted limit.

For strings

Check whether the prompt requires tests for:

  • Length: "", one character, two characters.
  • Repetition: all same character, repeated characters far apart, every character distinct.
  • Position: a relevant character at the beginning or end.
  • Formatting: leading spaces, trailing spaces, repeated separators, if the specification says whitespace matters.
  • Case: "A" versus "a" only if case sensitivity is specified or needs clarification.
  • Character model: non-ASCII text only if the problem says “characters” broadly rather than constraining input to lowercase English letters.

That last point matters in JavaScript. String indexing and .length operate on UTF-16 code units, which may not match a user-perceived character such as an emoji. For typical interview prompts that explicitly say lowercase English letters, do not complicate the solution with Unicode handling. For a general-text prompt, clarify the intended character definition before choosing a representation.


A practical interview testing routine

Use this routine after you explain your approach but before and after you code.

  1. State the relevant constraints.
    “I’m assuming the input may be empty, values can repeat, and the function returns only a Boolean.”

  2. Name three to five tests aloud.
    Choose a normal example plus the cases most likely to break the approach.

  3. Give expected outputs.
    This proves that you understand the specification, not only that you can name categories.

  4. Code the solution.

  5. Dry-run two tests.
    Use one boundary case and one case that challenges the main invariant.

A concise spoken version could be:

“Before coding, I’ll test an empty array, a single value, a duplicate separated by other values, and repeated values including zero. After implementation, I’ll dry-run the empty input and the separated-duplicate case to verify the loop boundaries and Set lookup.”

This takes little time and signals disciplined reasoning. It also protects against the frequent situation where a correct idea is implemented with one incorrect boundary condition.


Key takeaways

Edge-case testing begins with the contract: test valid inputs guaranteed or permitted by the prompt, and clarify whether invalid inputs need handling.

Construct tests systematically:

  • identify collection, value, position, frequency, and threshold boundaries;
  • test at a boundary and, when relevant, immediately around it;
  • target the assumptions and invariants of your actual approach;
  • write an expected output for every chosen input;
  • dry-run boundary and adversarial cases after coding.

For an interview solution, a small set of purposeful tests is better than a long, unfocused list. You now have the foundation needed to begin the next module, where frequency maps turn repeated scanning into structured counting for arrays and strings.

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

Sign up