Skip to content
Ameen Abozaid
PublishedBEG-057-L05

Demo: catching a subtle bug in generated binary search with tests

  • Lesson 5 of 5
  • 12 min read
  • Beginner
On This Lesson 11 topics

Lesson Overview

This demo walks through the whole module on one small, classic problem: binary search. You will write a requirement, write tests first, run a generated draft that looks perfectly reasonable, watch tests fail, trace one failure by hand, fix the code, and explain it.

Binary search is famous for subtle bugs: the idea takes one sentence, but the loop condition, the bounds, and the middle index are easy to get wrong. An assistant may produce a version that is almost right.

This is also the last lesson of the Beginner level, so after the demo we look back at how its modules fit together.

You need JUnit tests (BEG-045), the trace table (BEG-016 and lesson 2), the test-from-requirement method (lesson 3), and binary search and complexity from BEG-017 and BEG-039. This demo prepares the lab "Trust but verify".

Learning Objectives

By the end of this lesson you will be able to:

  • Carry out the full workflow (requirement, tests first, draft, trace, fix, explain) on a new method.
  • Explain why a "plausible" binary search can fail on the first item, the last item, and a one-item array.
  • Trace a failing case by hand and connect the trace to the loop condition.
  • State the complexity of binary search and the one-line reason.
  • Describe how the Beginner level's modules fit together, and what the next level builds on.

Written Explanation

Step 1: the requirement

Write it before anything else:

search(int[] sorted, int target) returns the index of target in an array sorted in ascending order. If target is absent, it returns -1. The array may be empty. If the array contains duplicates, any matching index is acceptable.

Decisions: -1 for absence (Java's own Arrays.binarySearch instead returns a negative number that encodes the insertion point, so our rule is deliberately different), empty array allowed, duplicates not our concern. A sorted input is a precondition: with an unsorted array, binary search gives an undefined result, and the Java documentation says the same for its own method.

Step 2: tests first

Before any draft, write the tests from the requirement (the checklist from lesson 3: boundaries, first, last, missing, one item):

java
class SearchTest {
    private final int[] four = {1, 3, 5, 7};
    @Test void emptyArray()           { assertEquals(-1, Search.search(new int[]{}, 5)); }
    @Test void oneItemFound()         { assertEquals(0, Search.search(new int[]{7}, 7)); }
    @Test void oneItemNotFound()      { assertEquals(-1, Search.search(new int[]{7}, 3)); }
    @Test void firstItem()            { assertEquals(0, Search.search(four, 1)); }
    @Test void lastItem()             { assertEquals(3, Search.search(four, 7)); }
    @Test void middleItem()           { assertEquals(2, Search.search(four, 5)); }
    @Test void missingBetween()       { assertEquals(-1, Search.search(four, 4)); }
    @Test void missingBelowAndAbove() { assertEquals(-1, Search.search(four, 0));
                                        assertEquals(-1, Search.search(four, 9)); }
}

Every expected value comes from the requirement: in {1, 3, 5, 7} the 7 is at index 3, and 4 is not there.

Step 3: the draft

Now paste the generated code:

java
public static int search(int[] sorted, int target) {
    int lo = 0, hi = sorted.length - 1;
    while (lo < hi) {
        int mid = lo + (hi - lo) / 2;
        if (sorted[mid] == target) return mid;
        if (sorted[mid] < target) lo = mid + 1;
        else hi = mid - 1;
    }
    return -1;
}

It compiles. Reading it for the idea (pass 1): keep a window [lo, hi], look at the middle, discard the half that cannot contain the target. That matches the requirement. The code even avoids the classic (lo + hi) / 2 overflow.

Step 4: run the tests

text
Tests run: 8, Failures: 3
SearchTest.firstItem:9 expected: <0> but was: <-1>
SearchTest.lastItem:10 expected: <3> but was: <-1>
SearchTest.oneItemFound:7 expected: <0> but was: <-1>

Five tests pass, so a quick manual try with 5 would have approved the draft.

Step 5: trace by hand

Take the smallest failing case, [7] with target 7:

Steplohilo < hi ?midAction
start000 < 0 is falseloop body never runs
endreturns -1

With one item, lo and hi are both 0. The window still contains one candidate, but lo < hi says "stop" because it treats a window of one item as empty. The loop never looks at it.

Now [1, 3, 5, 7] with target 7, which explains the last item failure:

Steplohilo < hi ?midsorted[mid]Action
103yes133 < 7, so lo = 2
223yes255 < 7, so lo = 3
3333 < 3 is falsestop, return -1

The 7 at index 3 was the last candidate when the loop quit. Target 1 fails the same way on the way down, while the middle items are found before the window shrinks to one item.

Diagnosis: the window [lo, hi] is inclusive, so lo == hi means one item remains and must be checked. The condition must be lo <= hi.

Step 6: fix and rerun

Change lo < hi to lo <= hi:

text
Tests run: 8, Failures: 0
BUILD SUCCESS

When lo > hi, the window is empty and -1 is correct. That also covers the empty array: hi starts at -1, so 0 <= -1 is false and the loop never runs.

Step 7: complexity and explanation

Each iteration discards half of the remaining window, so the number of iterations is about log2(n): for 1,000,000 items about 20 (2^20 is just over a million), against up to a million for a linear scan. Binary search is O(log n) time and O(1) extra memory. (This is arithmetic, not a measurement.)

Finally, explain the code in a comment:

java
// Keep the inclusive window [lo, hi] that could still contain target.
// Check the middle; discard it and the half that cannot contain target.
// lo <= hi: a window with lo == hi still holds one candidate, so it must be checked.

Add your disclosure line (lesson 4) to the README.

Looking back: how the Beginner level fits together

You now have the whole chain. By topic group:

ModulesWhat you learnedHow the module connects to the rest
BEG-001 to 007How computers, software, the internet, tools, and code execution workThe map that every later topic attaches to
BEG-008 to 018Java syntax, control flow, methods, arrays and strings, problem solving, algorithms, debuggingThe language and the thinking habits
BEG-019 to 028Object-oriented programming, from classes to design practiceHow programs are structured
BEG-029 to 038Exceptions, collections, generics, sorting, dates, files, streams, Optional, memoryThe standard toolbox of Java
BEG-039 to 044Complexity, data structures, hashing, trees, graphs, algorithmic techniquesChoosing and judging solutions
BEG-045 to 047, 054Testing, Git, GitHub, build toolsHow professionals keep code correct and shared
BEG-048 to 053, 055Data, relational modeling, SQL, PostgreSQL, web and API fundamentals, JDBCWhere backends keep data and how they talk to clients
BEG-056 to 057AI assistants and verifying their outputHow to use fast help without giving up responsibility

Where does it go next? The course roadmap lists the next level as the Junior Java Backend Engineer, whose stated promise is to build, test, secure, and deploy a real backend with Spring Boot and PostgreSQL. Its first module is titled "Modern Java", and a later module titled "AI-assisted development workflow" applies this module's habits to team work. We describe nothing beyond those titles and the roadmap's summary.

Examples

Example 1: the simplest test that finds it (simple)

The shortest route to the bug is search(new int[]{7}, 7): one line, one item, and the loop never runs. When time is short, empty and one-item tests are the cheapest, strongest tests you can write.

Example 2: a different generated bug, found by the same tests (practical)

Suppose another draft starts with hi = sorted.length (not length - 1) and keeps lo <= hi. Your tests from step 2 catch it too: emptyArray calls sorted[0] on an empty array, and missingBelowAndAbove (target 9) walks up to index 4 of a 4-item array. We ran it:

text
search(new int[]{}, 9)       -> ArrayIndexOutOfBoundsException: Index 0 out of bounds for length 0
search({1, 3, 5, 7}, 9)      -> ArrayIndexOutOfBoundsException: Index 4 out of bounds for length 4

Same tests, different bug: tests written from the requirement are not tied to one draft's mistakes.

Example 3: the overflow detail (realistic)

The draft uses lo + (hi - lo) / 2 instead of (lo + hi) / 2. Why? int has a largest value of 2,147,483,647 (Integer.MAX_VALUE, 2^31 - 1), and addition that goes past it wraps around to a negative number. We tried it with lo = 1,500,000,000 and hi = 2,000,000,000:

text
(lo + hi) / 2         -> -397483648
lo + (hi - lo) / 2    -> 1750000000

Arrays with billions of items are rare, but experienced readers check index arithmetic for this. The draft got it right; its bug was elsewhere, in a place that looked safe.

Backend Insight

Backends search constantly, and the "keep a window, halve it" idea appears in many places (database indexes are loosely similar, though real ones use richer structures; we did not verify that against a source).

The larger lesson is about trust. A service that is "almost right" on the first and last items of a list will fail on the first and last orders of every batch, which are exactly the rows that people notice. The cheap defence is the habit you practiced: a written requirement, tests from it, and a trace when a test fails.

Common Mistakes

Mistake 1: Judging a draft by its idea

The idea was correct; the bug was one character. Trace and test the boundaries.

Mistake 2: Writing only middle-of-the-array tests

The bug survived middleItem, missingBetween, and the missing-value tests. First, last, one-item, and empty are the tests that matter for loops with bounds.

Mistake 3: Fixing by trial and error

Changing < to <= is the right fix here, but know why: the window is inclusive, so a one-item window must be checked.

Exercises

Level A: Foundation

A1. Which test is the cheapest way to expose the lo < hi bug, and what does it tell you about the loop?

A2. Why does the test middleItem pass on the buggy draft?

A3. What is the complexity of binary search, and why?

Level B: Application

B1. Trace the buggy draft on [1, 3, 5, 7] with target 1. Give the table and the result.

B2. Write three more tests for search from the requirement that the draft in step 3 would also fail or that probe other boundaries (for example, a two-item array).

B3. The requirement says "any matching index is acceptable" for duplicates. Write a test for {2, 2, 2} with target 2 that is correct under that requirement, and say why assertEquals(0, ...) would be wrong.

Level C: Challenge

C1. Another draft uses lo <= hi but sets lo = mid (instead of mid + 1) when sorted[mid] < target. Trace it for [1, 3] with target 4 and say what goes wrong. Do not run it: why would running it be a poor idea without a safeguard?

C2. Write a short README section for this demo: requirement, defect, the test that caught it, the fix, complexity, and the disclosure line.

Show solutions

A1. The one-item test ([7] with target 7). It shows that the loop never runs when lo == hi, so the loop condition wrongly treats a one-item window as empty.

A2. The middle item is found at the first or second check, before the window shrinks to a single item, so the bad condition is never reached.

B1.

Steplohilo < hi ?midsorted[mid]Action
103yes133 > 1, so hi = 0
2000 < 0 is falsestop

Result: -1 (we ran it: expected 0, was -1). The 1 sits at index 0, where the window ended as one item.

B2. For example: search({1, 3}, 3) is 1; search({1, 3}, 2) is -1; search({1, 3, 5}, 5) is 2. On the buggy draft the first and third fail. Trace the first: lo = 0, hi = 1, mid = 0, 1 < 3 so lo = 1, then 1 < 1 is false and the method returns -1. (The second passes, and search({1, 3}, 1) would pass too, because 1 is found at the first middle check. We traced these by hand and did not run them.)

B3. For example: int i = Search.search(new int[]{2, 2, 2}, 2); assertTrue(i >= 0 && i <= 2); (or check that array[i] == 2). assertEquals(0, ...) would be wrong because the requirement allows any matching index; the algorithm happens to return index 1 first, and the test would break on a correct implementation.

C1. lo = 0, hi = 1, mid = 0, 1 < 4, so lo = mid = 0: nothing changed. The loop repeats with the same window forever. The method never returns, which is an infinite loop. Running it without a safeguard such as assertTimeout (lesson 3) would hang your test run. lo = mid + 1 is needed because mid was already checked and rejected.

C2. Answers vary. A good one has the requirement; "lo < hi skipped one-item windows"; "caught by oneItemFound, firstItem, lastItem"; "fixed with lo <= hi after tracing [7]"; "O(log n): each step halves the window"; and a disclosure line.

Mini Challenge

Repeat the demo on a method from the module lab: a hash map operation on your MyHashMap or the library borrowing rule. Write the requirement and at least eight tests first (including a boundary and an invalid input), then paste the draft and run. For each failure, trace the smallest failing input and decide whether code, test, or requirement is wrong. Record one defect, the test that caught it, and the fix.

Check your work: tests come before the draft, the defect record names a test, and the explanation is in your own words.

Summary & Next Lesson

Key takeaways

  • The workflow works: requirement, tests first, draft, trace, fix, explain.
  • A plausible draft can be right in the middle and wrong at the edges. In this demo, lo < hi failed for one-item windows, so first item, last item, and one-item array all failed, while the middle item passed.
  • A trace of the smallest failing case points directly at the faulty condition.
  • Binary search is O(log n) because each step halves the window.
  • The Beginner level is a connected chain: how computers work, the language, objects, the library, algorithms, professional tools, data and the web, and now responsible use of AI.

Next lesson

This is the last lesson of the Beginner level. The next step is the lab "Trust but verify" (BEG-057-LAB1), and after that the course continues with the Junior Java Backend Engineer level, which starts with a module titled "Modern Java".

Self-check

  1. Which tests failed on the lo < hi draft?
  2. Why did a quick manual check with the middle item mislead?
  3. What does lo <= hi mean in terms of the search window?
  4. What is the complexity of binary search?
  5. Name one thing from earlier modules that this demo relied on and what it was used for.
Show answers
  1. oneItemFound, firstItem, and lastItem.
  2. The middle item is found before the window shrinks to one item, so the faulty condition is never reached.
  3. The window [lo, hi] is inclusive, so lo == hi means one candidate remains and must still be checked; the window is empty only when lo > hi.
  4. O(log n) time and O(1) extra memory.
  5. For example: JUnit (BEG-045) to run the tests, the trace table (BEG-016) to find the faulty condition, or complexity (BEG-039) to state the cost.

References

  • Oracle, Java SE 21 Arrays API. For binarySearch(int[], int) it states that the array must be sorted, that the result is undefined otherwise, that a duplicate key may return any matching index, and that a missing key gives -(insertion point) - 1.
  • Oracle, Java SE 21 Integer API. It gives Integer.MAX_VALUE as 2^31 - 1 (2,147,483,647). That addition past it wraps around follows from two's complement arithmetic, which the page describes only indirectly; we confirmed the wrap by running the code.
  • All tests and outputs in this lesson were run with JDK 21 and JUnit 5.12.2 in a scratch Maven project. The roadmap summary of the next level comes from the course's own roadmap file. The remark that database indexes are loosely similar to binary search was not verified and is hedged in the text.

Practice

Apply this lesson with the module's lab, exercises and quiz.

Lab and exercises Quiz

Marking a lesson complete is optional and you can undo it any time. Your progress stays in this browser.

Type to search levels, phases, modules and lessons.