Create your own
Lesson illustration

Strategic Software Testing Techniques

Welcome back! In our first lesson, we focused on the crucial initial step of problem-solving: methodically deconstructing a problem statement to understand its inputs, outputs, and rules. This systematic approach is the best defense against the anxiety of facing an unfamiliar problem.

Today, we'll take the next logical step. Once you've clarified what a problem is asking, how do you verify that your understanding is correct and ensure your future solution will be robust? The answer is by creating a strong set of test cases. This lesson will teach you how to construct representative, boundary, and adversarial examples that will become your trusted tools for validating your logic and catching bugs before they happen.

From Understanding to Verification

In your career as a front-end developer, you know that code isn't "done" until it's tested. The same principle applies to algorithms, but we can—and should—start the testing process before writing any code. Creating examples forces you to translate the abstract rules of the problem into concrete inputs and expected outputs. This process often reveals subtle misunderstandings or overlooked edge cases.

In the following video, Anthony D. Mays, a former Google software engineer and interview coach, explains why working through examples is a non-negotiable step in the interview process.

How to Solve ANY Coding Interview Question in 6 Steps

Watch this segment where Anthony discusses the importance of creating examples.

Pay close attention to how he frames this step, from thinking about examples to how it helps you communicate with the interviewer and correct misunderstandings early.

As he points out, this isn't just about checking your own understanding; it's a way to confirm with an interviewer that you're both on the same page. A good set of examples acts as a contract for what your algorithm must accomplish.

A Framework for Test Cases

So, what makes a "good" set of test cases? A thorough set covers three categories, moving from the common to the tricky.

  1. Representative Cases: These are the "happy path" or typical scenarios. They test the main logic of the problem and are usually easy to come up with.
  2. Boundary Cases: These test the edges or limits of the input constraints. This is where a huge number of bugs, like the infamous "off-by-one" error, originate.
  3. Adversarial Cases: These are "unfriendly" inputs designed to break a naive algorithm. They often combine multiple boundary conditions or exploit specific tricky aspects of the problem.

Let's explore these in more detail.

Identifying Boundaries and Partitions

To be systematic about finding boundary cases, we can borrow two powerful concepts from the world of software quality assurance: Equivalence Partitioning and Boundary Value Analysis (BVA).

Equivalence Partitioning is the idea of dividing all possible inputs into groups, or "partitions," where the system is expected to behave similarly for every input in that group. For a problem accepting an age between 18 and 60, we might identify three partitions: age < 18 (invalid), 18 <= age <= 60 (valid), and age > 60 (invalid).

This diagram illustrates equivalence partitioning. For an input field that accepts numbers from 1 to 100, we can define partitions for valid inputs (e.g., Partition 2: 1-100) and invalid inputs (e.g., Partition 1: < 1, Partition 3: > 100). We only need to test one value from each partition.

Boundary Value Analysis (BVA) takes this a step further. It states that bugs are most likely to occur at the "edges" of these partitions. So, instead of testing a random value from the middle of a partition (like age 35), BVA directs us to focus on the values right at and on either side of the boundaries.

This diagram shows Boundary Value Analysis for a valid range of 1 to 100. Test cases are strategically chosen at the boundaries: the minimum (1), maximum (100), just inside the boundaries (2, 99), and just outside the boundaries (0, 101).

The following blog post from Keploy gives a great, practical overview of BVA.

Boundary Value Analysis (BVA) in Software Testing | Keploy Blog

This article provides a concise explanation of BVA, a technique that is fundamental to creating robust test cases.

Please read the introduction, the definition of BVA, and the section on Core Concepts. Finally, review the worked example for an age field to see how test cases are created.

Building Your Edge Case Checklist

The concepts from BVA give us a great starting point. The clarifying questions we discussed in the last lesson (e.g., "Can the array be empty?", "Can numbers be negative?") are a goldmine for identifying important boundaries.

In the next video, Tech With Tim discusses how to create your own test cases, explicitly mentioning the need to think about edge cases.

How To Pass Coding Interviews Like the Top 1%

Watch this segment where Tim talks about creating your own test cases.

Focus on the part from where he discusses test cases. He specifically mentions thinking about various edge cases like empty inputs, negative values, and duplicate values.

To make this process repeatable, it's helpful to have a mental checklist of common edge cases. The article below provides an excellent list.

Thinking in Edge Cases: How to Bulletproof Your Coding Solutions in Interviews – AlgoCademy Blog

This article categorizes the most common types of edge cases you'll encounter in interviews.

Read the section Common Edge Cases. Pay attention to the five categories: Empty Collections, Large Inputs, Negative Numbers, Boundary Values, and Duplicate Elements. This will serve as your starting checklist for any problem.

Putting it All Together: A Worked Example

Let's apply this structured thinking to the problem from our last lesson's exercise: "Given a string s, find the first character that is not repeated anywhere else in the string."

Let's assume we've asked clarifying questions and established that:

  • The input is a string of lowercase English letters.
  • If no non-repeating character is found, we should return a special indicator (e.g., an empty string "").
  • The string length can be from 0 to 50,000.

Here's how we'd build our test cases:

  1. Representative Cases (Happy Path):

    • s = "leetcode" -> Expected: "l" (The first char is non-repeating)
    • s = "loveleetcode" -> Expected: "v" (The first non-repeating char is in the middle)
  2. Boundary Cases (Using our checklist):

    • Empty Collection: s = "" -> Expected: ""
    • Single Element: s = "a" -> Expected: "a"
    • All Duplicates: s = "aabbcc" -> Expected: ""
    • Last character is the answer: s = "aabbc" -> Expected: "c"
    • Input at max size: A string of 49,999 'a's followed by one 'b'. s = "aa...ab" -> Expected: "b"
  3. Adversarial Cases (Trying to be "mean"):

    • All characters unique: s = "abcdefg" -> Expected: "a" (Tests if the logic correctly stops at the very first one).
    • Only one character repeated: s = "abacaba" -> Expected: "c" (This pattern can sometimes fool simple counting mechanisms).

With these examples, we have a high degree of confidence that any algorithm passing all of them is correct.

Your Turn: An Exercise

Now, it's your turn to create a set of test cases. Consider the following problem:

Problem: "Given a sorted array of integers nums and an integer target, return the starting and ending position of the target value as an array [start, end]. If the target is not found, return [-1, -1]."

Using the three categories (representative, boundary, adversarial), list at least five test cases for this problem, including the input nums and target, and the expected output.

Click here to see a sample set of test cases.

Here is a possible set of test cases:

  • Representative Case:

    • Input: nums = [5, 7, 7, 8, 8, 10], target = 8
    • Expected Output: [3, 4]
  • Boundary Cases:

    • Target not found:
      • Input: nums = [5, 7, 7, 8, 8, 10], target = 6
      • Expected Output: [-1, -1]
    • Empty array:
      • Input: nums = [], target = 5
      • Expected Output: [-1, -1]
    • Target is the only element:
      • Input: nums = [5], target = 5
      • Expected Output: [0, 0]
    • Target at the start of the array:
      • Input: nums = [5, 5, 7, 8], target = 5
      • Expected Output: [0, 1]
    • Target at the end of the array:
      • Input: nums = [5, 7, 8, 8], target = 8
      • Expected Output: [2, 3]
  • Adversarial Case:

    • All elements are the target:
      • Input: nums = [8, 8, 8, 8, 8], target = 8
      • Expected Output: [0, 4]

Conclusion

In this lesson, we transitioned from understanding a problem to creating concrete examples to test that understanding. This is a critical engineering practice that transforms abstract problem-solving into a verifiable, confidence-building process.

Here are the key takeaways:

  • Test Before You Code: Create examples immediately after you understand the problem. This solidifies your understanding and provides a safety net for your future solution.
  • Use a Structured Approach: Don't just pick examples randomly. Systematically create representative, boundary, and adversarial test cases.
  • Leverage BVA and Checklists: Use Boundary Value Analysis to focus on the edges of input ranges, and maintain a mental checklist of common edge cases (empty inputs, singletons, duplicates, etc.).

In our next lesson, we'll see exactly why these test cases are so valuable. We will learn how to trace an algorithm on a small example, and our test cases will provide the perfect inputs for that tracing process.

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

Sign up