Testing and Debugging DSA Solutions
Testing and debugging is what turns “it compiles” into “it’s correct.” The fastest way to find a bug in a DSA solution is to feed it small, hand-checkable inputs and the nasty edge cases — long before you submit it against the full constraints. This page shows which cases to test, how to debug systematically, and a tiny reusable test harness in all four languages.
Test the smallest cases first
A function that works on [5, 2, 9, 1] but not [] is still broken. Trace tiny inputs by hand, then assert the result in code. The order to try:
- Empty input —
[],"", an empty tree. - One element — does the loop body run? Do off-by-one bounds hold?
- Two elements — the smallest case where order matters.
- A small typical case you can verify by hand.
If a small case fails, you’ll find the bug in seconds. If you only test large random inputs, you’ll know it’s wrong but not where.
The edge cases that catch real bugs
Keep this checklist next to your screen:
- Empty / one element — boundary of every loop.
- Duplicates — do you double-count, or skip a valid pair?
- All equal / all negative — breaks
best = 0style initialization. - Already sorted / reverse sorted — worst case for quicksort, partition logic.
- Overflow — sums and products that exceed 32-bit range (see common mistakes).
- Max size — does an O(n²) idea time out at n = 10⁵?
For beginners: “Edge case” just means an input at the edge of what’s allowed — the smallest, the largest, or the unusual shape. Most graders are built specifically to hit these, so test them yourself first.
A tiny test harness
You don’t need a framework. A handful of assert-style checks that compare actual vs expected output catch almost everything. Here is a self-contained harness testing a sum function, including the empty case:
#include <cassert>
#include <vector>
long long sum(const std::vector<int>& a) {
long long total = 0;
for (int x : a) total += x;
return total;
}
int main() {
assert(sum({}) == 0); // empty
assert(sum({7}) == 7); // one element
assert(sum({1, 2, 3}) == 6); // typical
assert(sum({-2, -3}) == -5); // negatives
return 0; // all passed if no abort
}
public class Main {
static long sum(int[] a) {
long total = 0;
for (int x : a) total += x;
return total;
}
static void check(boolean cond, String name) {
if (!cond) throw new AssertionError("FAILED: " + name);
}
public static void main(String[] args) {
check(sum(new int[]{}) == 0, "empty");
check(sum(new int[]{7}) == 7, "one element");
check(sum(new int[]{1, 2, 3}) == 6, "typical");
check(sum(new int[]{-2, -3}) == -5, "negatives");
System.out.println("all passed");
}
}
function sum(a) {
let total = 0;
for (const x of a) total += x;
return total;
}
function check(cond, name) {
if (!cond) throw new Error("FAILED: " + name);
}
check(sum([]) === 0, "empty");
check(sum([7]) === 7, "one element");
check(sum([1, 2, 3]) === 6, "typical");
check(sum([-2, -3]) === -5, "negatives");
console.log("all passed");
def sum_all(a):
total = 0
for x in a:
total += x
return total
assert sum_all([]) == 0 # empty
assert sum_all([7]) == 7 # one element
assert sum_all([1, 2, 3]) == 6 # typical
assert sum_all([-2, -3]) == -5 # negatives
print("all passed")
Each case names the scenario it covers, so a failure tells you which shape of input broke. Add a new assert the moment you think of a tricky input — your harness grows into a safety net.
Debug systematically, not randomly
When a case fails, resist random edits. Instead:
- Reproduce with the smallest failing input.
- Predict what each line should produce, then print or step through to find the first line that disagrees — that’s your bug, not the line that crashes.
- Check the usual suspects: loop bounds (
<vs<=), the update inside the loop, the initial value, and the return for the edge case. - Fix one thing, re-run the whole harness (a fix can break another case).
Tip: A “brute-force oracle” finds bugs you can’t see. Write a slow-but-obviously-correct version, generate random inputs, and compare it against your fast solution. The first mismatch is a minimal failing test — this is how competitive programmers catch rare edge cases.
Going deeper: Print sparingly and meaningfully. Dumping a whole array every iteration buries the signal. Print the decision a branch makes (
"taking left, lo=3 hi=5") so the log reads like the algorithm’s reasoning.
A testing checklist
- Empty, one, and two-element inputs all pass.
- Duplicates, negatives, and all-equal inputs pass.
- Sums/products use a wide enough type (no overflow).
- The largest allowed input finishes in time.
- Every bug you fix gains a permanent test.
With tests in place, you’ll spot patterns in the bugs you make — many are on the common mistakes list.