Skip to content
Ameen Abozaid
PublishedBEG-045-L06

Reading test failures

  • Lesson 6 of 7
  • 11 min read
  • Beginner
On This Lesson 11 topics

Lesson Overview

A good suite of tests is green most of the time and red when something is wrong. When it goes red, the result is useful only if you can read it. This short lesson teaches you to read a test failure: what each part of the message means, how to find the line that matters, and the first question to ask before you change anything: is the code wrong, or is the test wrong?

You have already seen real failure messages in the previous lessons. Here we slow down and read them properly. The inspector analogy fits: a red result is an inspection report, and a report is only helpful if you can tell which item failed, what was found, and what was expected.

The examples use a tiny ShoppingList class and a Greeter, plus our Account. Output is from JUnit 5.12.2 on JDK 21, through a small launcher that prints the failure and the lines of the stack trace that belong to our code. Your IDE shows the same information in a results window, with more lines and clickable links.

Learning Objectives

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

  • Read an assertion failure message and say which value the code returned.
  • Tell an assertion failure from an exception thrown inside the code.
  • Find, in a stack trace, the line in your own code where a failure started.
  • Decide whether the code or the test is at fault.
  • Follow a repeatable routine: read, locate, decide, fix, rerun everything.
  • Write a failure message for an assertion, when the default is not clear enough.

Written Explanation

The parts of a failure

A failed test shows you four things:

  1. The test's name, for example threeAddsGiveSizeThree(). This is why behavior names matter.
  2. The kind of failure: an assertion failure (AssertionFailedError) or an exception from the code.
  3. The message: for assertEquals, expected: <3> but was: <2>.
  4. A line number or stack trace pointing into your code.

Reading expected ... but was ...

For assertEquals(expected, actual), the message looks like this (real output):

text
org.opentest4j.AssertionFailedError: expected: <3> but was: <2>
    at FailureDemoTest.threeAddsGiveSizeThree(FailureDemoTest.java:11)

The value after expected is what your test said should happen; the value after but was is what the code actually returned. Here the code said 2. (This is exactly why expected-first, actual-second matters from lesson 3: with swapped arguments the labels would lie.) The last line says that the failing assertion is on line 11 of the test file. In an IDE, that line is usually a link that jumps to the source.

Assertion failures vs exceptions from the code

There are two broad kinds of red result:

  • Assertion failure: the code ran to the end and gave an answer, but the answer was not the expected one. Read the expected and actual values.
  • Exception inside the code: the code crashed before it could answer. The message is the exception's own message, plus a stack trace.

IDEs often show these with different icons or colors (sometimes called a failure and an error), but how they are labeled depends on the tool, so go by what the message says. JUnit itself marks both as failed tests.

Reading a stack trace

A stack trace lists the chain of method calls that led to the exception, most recent call first. Real output from a test whose code crashed:

text
java.lang.NullPointerException: Cannot invoke "String.toUpperCase()" because "name" is null
   at Greeter.shout(Greeter.java:3)
   at Badge.label(Greeter.java:9)
   at FailureDemoTest.badgeShowsShoutedName(FailureDemoTest.java:16)

(The message names the variable name because the code was compiled with debug information, which IDEs normally do; compiled without it you may see a placeholder like <parameter1>.) Read it from the top:

  • The first line says what happened: a null was used where a String method was called, and which variable was null.
  • Greeter.shout(Greeter.java:3) is where: line 3 of Greeter, the line that called toUpperCase().
  • The next lines are the callers: Badge.label called shout, and the test called label, on line 16 of the test.

A real trace also contains many lines from JUnit and the JDK (our launcher hid them). Ignore the lines from libraries and look at the top lines that name your code. The top line is where the crash happened; the bottom line from your code is the test that triggered it.

Is the code wrong, or is the test wrong?

Before editing anything, decide which side is mistaken. Both are possible, and "fixing" the wrong one hides a bug or creates one.

Look at our first failure again: three add calls (milk, eggs, milk) and an expected size of 3. The code returned 2, because ShoppingList.add ignores an item that is already there. So whose mistake is it? It depends on the requirement:

  • If a shopping list should keep duplicates, the code is wrong; fix add.
  • If it is meant to ignore duplicates, the test is wrong; change the expected value to 2, and give the test a name that records the rule, like duplicateItemIsIgnored.

The test cannot answer this; the requirement does. When there is no written requirement, ask whoever owns it. Changing an expected value just to turn the bar green, without deciding which side is correct, is the testing equivalent of unplugging a smoke alarm.

A routine for failures

  1. Read the message: expected, actual, and the test's name.
  2. Locate the line: open the failing assertion or the first line of your code in the stack trace.
  3. Decide: is the code or the test wrong? Check the requirement.
  4. Fix one thing.
  5. Rerun the failing test, then rerun all tests. A fix can break something that was passing, which is exactly the regression lesson 1 described.

For the question "why is list.size() 2 and not 3 after three adds?", this routine gives a short checklist: confirm the three add calls really ran, check whether any call is rejected or ignored, and look at whether add increments the count. In our case the second milk was ignored by the contains check.

Helping the failure message

Sometimes expected: <true> but was: <false> is too vague. Most assertions accept an optional message as their last argument, which JUnit prints on failure:

java
assertTrue(AgeRules.isAdult(18), "18 should count as an adult");

The JUnit documentation notes that the failure message is the last parameter. Use messages sparingly: a good test name often says enough.

Examples

Example 1: a plain assertion failure (simple)

java
@Test
void threeAddsGiveSizeThree() {
    ShoppingList list = new ShoppingList();
    list.add("milk");
    list.add("eggs");
    list.add("milk");
    assertEquals(3, list.size());
}
text
FAILED  threeAddsGiveSizeThree()
   org.opentest4j.AssertionFailedError: expected: <3> but was: <2>
      at FailureDemoTest.threeAddsGiveSizeThree(FailureDemoTest.java:11)

Read it aloud: "Test threeAddsGiveSizeThree failed. The test expected 3 but the code returned 2. Look at line 11." Then decide whether duplicates should count, as discussed above.

Example 2: an exception from inside the code (practical)

java
@Test
void badgeShowsShoutedName() {
    assertEquals("[ANA!]", new Badge().label(new Greeter(), null));
}
text
FAILED  badgeShowsShoutedName()
   java.lang.NullPointerException: Cannot invoke "String.toUpperCase()" because "name" is null
      at Greeter.shout(Greeter.java:3)
      at Badge.label(Greeter.java:9)
      at FailureDemoTest.badgeShowsShoutedName(FailureDemoTest.java:16)

There is no expected and but was, because the code never returned. The cause is on the top line that names our code: Greeter.shout, line 3. The test passed null for the name. Here the test gave bad input, so ask the same question as before: should shout(null) work, throw a clear exception, or is the test's null a mistake? That is a design decision, not a typo.

Example 3: reading a boundary failure (realistic)

In lesson 5 we planted amount >= balance into withdraw. The result was:

text
FAILED  exactlyTheBalanceIsAllowed()
   InsufficientFundsException: Cannot withdraw 100.00: balance is only 100.00
      at Account.withdraw(Account.java:25)
      at WithdrawBoundaryTest.exactlyTheBalanceIsAllowed(WithdrawBoundaryTest.java:22)

Walk through it with the routine. Read: an unexpected InsufficientFundsException (the test expected the withdrawal to work), and the message says the request equals the balance. Locate: the top line of our code is Account.withdraw, line 25, the line that throws. Decide: the rule says a withdrawal of exactly the balance is legal, so the code is wrong. Fix: change >= back to >. Rerun the test, then all tests. Four short steps from a red bar to a green one, because the test name, the message, and the line number all pointed the same way.

Backend Insight

In a team, you will often read someone else's failing test, or a failure from an automated build you did not run on your own computer. The same reading skills apply: the test name tells you the behavior, the message tells you the difference, and the top lines of the stack trace that name your project tell you where to look. Build servers show the same text, so a failure you can read on your machine is a failure you can read in a team's build report. Habit to build now: read the whole message before changing any code, and say the failure aloud in one sentence.

Common Mistakes

Mistake 1: Changing the expected value to make the test green

  • Incorrect: the test says 3, the code returns 2, so change the test to 2 without checking the requirement.
  • Correct: decide from the requirement which side is wrong; change that side only.

Mistake 2: Reading only the last line of the stack trace

The bottom line is often the test or the framework. The line that matters most is usually the top one that names your own code.

Mistake 3: Rerunning only the failing test

After a fix, rerun everything. Otherwise you cannot know whether the fix broke another test.

Mistake 4: Ignoring the message and guessing

Reading expected and but was carefully, and the exception's text, answers most questions in seconds. Guessing and editing at random wastes much more time.

Exercises

Level A: Foundation

A1. A test reports expected: <150> but was: <100>. What did the code return?

Show solution A1

A1. 100: the value after but was is the code's answer.

A2. In a stack trace, which end lists the most recent call?

Show solution A2

A2. The top. The first line is the most recent call, and the lines below it are the callers.

A3. Name the five steps of the routine for a failing test.

Show solution A3

A3. Read the message; locate the line; decide whether the code or the test is wrong; fix one thing; rerun the failing test and then all tests.

Level B: Application

B1. Read this failure and say what happened, where, and which side to check first:

text
org.opentest4j.AssertionFailedError: expected: <60> but was: <100>
    at AccountTest.withdrawSubtractsFromBalance(AccountTest.java:14)
Show solution B1

B1. The test withdrawSubtractsFromBalance expected a balance of 60 but the code returned 100, so the balance did not change. Look at line 14 of AccountTest, then at withdraw: it may not be reaching the line that subtracts, or the test may not be calling it as intended. The requirement says a withdrawal subtracts, so check the code first.

B2. Read this one in the same way:

text
java.lang.IndexOutOfBoundsException: Index 3 out of bounds for length 3
    at MyList.get(MyList.java:21)
    at ListTest.lastItemIsReadable(ListTest.java:9)
Show solution B2

B2. The code threw an IndexOutOfBoundsException at line 21 of MyList.get, when the test (line 9) read index 3 of a list of length 3. Valid indexes are 0 to 2, so either the test is off by one (the last item is index 2) or get should have been reading from a different index. The test name says "last item is readable", so check whether the test used size() instead of size() - 1.

B3. After a fix, the failing test is green but another test is now red. What does this tell you?

Show solution B3

B3. The fix changed behavior that the second test depended on: a regression, caught because you reran everything. Reconsider the fix, or decide whether the second test, rather than the code, expressed the right behavior.

Level C: Challenge

C1. A test expects assertEquals(0.3, a + b) where a = 0.1 and b = 0.2, and fails with expected: <0.3> but was: <0.30000000000000004>. Is the code or the test at fault, and how do you fix it?

Show solution C1

C1. The test is at fault: the code is performing ordinary floating-point arithmetic, and 0.1 + 0.2 is not exactly 0.3 in binary. Fix the test with a delta: assertEquals(0.3, a + b, 0.0001).

C2. The failure is expected: <2> but was: <3> for list.size() after two adds and one remove. Write three questions you would ask before changing anything.

Show solution C2

C2. Possible questions: Did the remove actually run, and did it find the item? Is it meant to remove one item, or all matching items? Does size get decremented in remove? (The key point: check what the code does against the requirement before changing the expected value.)

Mini Challenge

Take the five tests you wrote in earlier mini challenges (or any small test class). Plant three different bugs in the code, one at a time. For each, run the tests and write down, in one sentence each, the test name, what was expected, what the code returned, and the line it points to. Then fix the bug and rerun all the tests.

Check your work: each sentence names the behavior in plain words, you looked at the top code line of any stack trace, and you reran the whole suite after each fix, not just the failing test.

Summary & Next Lesson

Key takeaways

  • A failure shows the test name, the kind of failure, a message, and a line or stack trace.
  • expected: <X> but was: <Y> means the code returned Y.
  • For an exception, read the top lines of the stack trace that name your code.
  • Before fixing, decide whether the code or the test is wrong, using the requirement.
  • Fix one thing, rerun the failing test, then rerun all tests.

Next lesson

The next lesson, "Demo: test-first on the bank's withdraw method", puts everything together: you will write the tests for withdraw first, watch them fail, make them pass, and read a planned failure along the way.

Self-check

  1. A test says expected: <5> but was: <4>. Which value did the code return?
  2. Where in a stack trace do you look first?
  3. What is the first question to ask when a test fails?
  4. Why rerun all tests after a fix?
  5. What does an optional message in assertTrue(cond, "text") do?
Show answers
  1. 4.
  2. At the top line that names your own code.
  3. Whether the code or the test is wrong, judged against the requirement.
  4. A fix can break other behavior that was passing (a regression).
  5. JUnit prints it when the assertion fails, to explain what was expected.

References

  • JUnit documentation, Writing Tests: Assertions. Supports that the optional failure message is the last parameter of an assertion; its examples put the expected value first.
  • JUnit 5 Assertions Javadoc. Supports that the first two parameters are named expected and actual, and that failed assertions throw AssertionFailedError (or a subclass).
  • The failure and stack-trace output above is real output from JUnit 5.12.2 on JDK 21, trimmed to the lines from our own classes. The wording of the NullPointerException message and the IDE's labels for failures and errors may differ between JDK versions and tools; I did not verify the JDK version in which that message wording was introduced. I did not verify how particular IDEs label failures versus errors.

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.

Continue to next lesson

Type to search levels, phases, modules and lessons.