Concepts / Debugging Test Failures

Debugging Test Failures

The find_all() test loops over every match and reports a problem only when a match does not hold the character being searched for, so a passing run prints nothing.

  • Programming

Reading a Test Before Running It

Many programs are checked by small tests. A test runs a function on known input and looks for anything unexpected in what comes back. The test in this article checks find_all(), a function that searches a string for every occurrence of one character. Before reading any explanation of how it works, read the test itself and notice what it does with each result it receives.

The test contains no list of expected output and no counting. It makes one promise: every position that find_all() reports should hold the letter 'a'. If that promise is broken even once, a message appears. The test is therefore about the quality of each individual result, not about how many results there are.

What do you think happens?

Run the find_all() test on the text Mary had a little lamb. What do you expect the test to print?

  • Nothing is printed

  • The message a bug! is printed once for each match

  • The message a bug! is printed once, at the first match

Reveal answer
Answer:

Nothing is printed.

Every position that find_all() reports holds an 'a', so no check fails and the message never appears. A passing run prints nothing.

Tracing the Mary Had a Little Lamb Text

To see the test succeed in detail, trace the string by hand. Positions count from zero, which is how positions in a C++ string are numbered. The letter M sits at position 0, the letter a at position 1, and so on through the whole sentence. The letter 'a' appears at four positions in this text: 1, 6, 9, and 19. These are the matches find_all() should report, and each one should pass the test's check.

PositionCharacter at that positionDoes the check *p equal 'a' hold?What the test does
1aYesNothing is printed
6aYesNothing is printed
9aYesNothing is printed
19aYesNothing is printed
2 (only if find_all() were broken)rNoThe message a bug! is printed

A correct result at each reported position, with one broken result shown for comparison

What the Loop Variable Holds

The test loops over every match that find_all() returns. Inside the loop, a variable named p refers to one match at a time. The check inside the loop compares the character that p refers to with the character 'a'. The expression that reads that character is written *p, and it means the value stored at the place p refers to. The check is written in this form:

Notes on the code
  • *p reads the character that the iterator p refers to
  • 'a' is the character every reported match should hold
  • The message is printed only when the condition is true

Comparing the value that p refers to, rather than p itself, is what makes the test examine the characters and not the positions. The iterator is the thing that moves from match to match. The value is what the rule is about. Because the test reads the value, it can check whether each match holds the right letter, without caring where that match sits in the string.

Next match fromfind_all()Does *p equal 'a'?Nothing printedYes branchMove to next matchTest finishesNo matches remainPrint a bug!No branchcheckyesnorepeat for each matchloop ends
For each match the loop receives, the check on *p decides whether the message appears. The loop then moves on, and when no matches remain the test finishes.

Locating the First Divergent Match

Now suppose find_all() were broken and reported a position that holds a different letter, such as the r at position 2. The loop would reach that result, the check *p equal to 'a' would fail, and the message a bug! would appear. The output alone tells you that a mismatch happened, but not which result caused it. The divergence is at the single loop iteration where the character under p is not 'a'.

Input stringMary had a little lambfind_all()searches for 'a'Reported position 2holds rLoop variable prefers to the r*p != 'a' is truea bug! printedsearchreturnsone iterationchecktrue
The path runs from the input string through find_all() to the loop variable. The wrong match first appears when find_all() reports position 2, because that position holds r and the check on *p is then false.

A practical way to pin the failure down is to print the position or the character inside the loop as well. This turns a single error message into a record of every match that was checked. Once you can see that sequence, the first unexpected entry marks where the result diverged from what you expected. In the broken example, the record would show positions 1, 2, 6, 9, and 19, and the entry for position 2 is where the trouble starts.

Why find_all() Is Not Limited to Strings

So far find_all() has been used on a string, but the idea behind it is not tied to strings. The principle that makes this possible is that iterators and standard algorithms behave the same way on every standard container for which their use makes sense. An algorithm does not need to know which kind of container it is working with, as long as the container provides the iterators the algorithm needs.

Containing elementChild item 1Matches returnedChild item 2holdsholdsmatchmatch
A search over a containing element returns the items it holds. Each returned match relates back to the container and to its children, which is why the same search can be written for other containers as well as strings.

Because of this, the source suggests that find_all() can be generalized. Instead of searching only a string for a character, the same search can be written so that it works on other standard containers. The test you read earlier would still express the same expectation in that case: every match reported should satisfy the condition being searched for. The test does not change its promise when the container changes. Only the container being searched changes.

Mistakes When Reading a Silent Test

  • Common mistake

    Assuming a silent test never ran

    What it looks like

    The test prints nothing for Mary had a little lamb and the learner concludes that the test did nothing.

    Why it fails

    The loop runs once for each match. A silent run still means find_all() produced matches and each of them was checked.

    Do this instead

    Remember what the loop does. Silence means every checked match passed.

  • Common mistake

    Assuming the test counts results

    What it looks like

    Expecting the test to report that four a characters were found.

    Why it fails

    The test contains no counting and no expected-output list. It only checks the quality of each result.

    Do this instead

    Read the test as a check on each individual match, not as a tally.

  • Common mistake

    Comparing the iterator instead of its value

    What it looks like

    Writing the check as p != 'a' instead of *p != 'a'.

    Why it fails

    p refers to a position, while *p is the character stored there. Comparing p checks positions, not characters.

    Do this instead

    Use *p so the check reads the character that the iterator refers to.

  • Common mistake

    Stopping at the error message instead of printing the sequence of matches

Tracing a Broken Result

MEDIUM

Suppose find_all() on Mary had a little lamb reports the positions 1, 6, 9, 13, and 19, where position 13 holds a different letter from 'a'. Using the position table from earlier, identify which reported position is the first divergent match, state what the loop prints for it, and explain how printing the position inside the loop would help you find it.

Hints
  • Count from zero when you locate each reported position in the text
  • Only the loop iteration whose value is not 'a' triggers the message
  • Think about which entry in a printed record would stand out as the first unexpected one

Key Takeaways

  • The find_all() test loops over every match and reports a problem only when a match does not hold the character being searched for, so a passing run prints nothing.
  • The check compares the value that the loop iterator refers to, written *p, with 'a'. This is why the test examines characters rather than positions.
  • A wrong match first appears at the single loop iteration where the value under p breaks the rule. Printing positions or characters inside the loop turns one error message into a record that shows where the result diverged.
  • Standard iterators and algorithms behave the same way on every standard container for which their use makes sense, so find_all() can be generalized beyond strings while the test's expectation stays the same.

Related concepts

More concepts starting with D