قراءة إخفاقات الاختبار
في هذا الدرس 11 موضوعات
هذا الدرس متاح حاليًا باللغة الإنجليزية فقط، ولم تُنشر نسخته العربية بعد.
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:
- The test's name, for example
threeAddsGiveSizeThree(). This is why behavior names matter. - The kind of failure: an assertion failure (
AssertionFailedError) or an exception from the code. - The message: for
assertEquals,expected: <3> but was: <2>. - 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):
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:
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
Stringmethod was called, and which variable was null. Greeter.shout(Greeter.java:3)is where: line 3 ofGreeter, the line that calledtoUpperCase().- The next lines are the callers:
Badge.labelcalledshout, and the test calledlabel, 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
- Read the message: expected, actual, and the test's name.
- Locate the line: open the failing assertion or the first line of your code in the stack trace.
- Decide: is the code or the test wrong? Check the requirement.
- Fix one thing.
- 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:
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)
@Test
void threeAddsGiveSizeThree() {
ShoppingList list = new ShoppingList();
list.add("milk");
list.add("eggs");
list.add("milk");
assertEquals(3, list.size());
}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)
@Test
void badgeShowsShoutedName() {
assertEquals("[ANA!]", new Badge().label(new Greeter(), null));
}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:
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?
أظهر الحل 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?
أظهر الحل 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.
أظهر الحل 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:
org.opentest4j.AssertionFailedError: expected: <60> but was: <100>
at AccountTest.withdrawSubtractsFromBalance(AccountTest.java:14)أظهر الحل 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:
java.lang.IndexOutOfBoundsException: Index 3 out of bounds for length 3
at MyList.get(MyList.java:21)
at ListTest.lastItemIsReadable(ListTest.java:9)أظهر الحل 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?
أظهر الحل 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?
أظهر الحل 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.
أظهر الحل 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
- A test says
expected: <5> but was: <4>. Which value did the code return? - Where in a stack trace do you look first?
- What is the first question to ask when a test fails?
- Why rerun all tests after a fix?
- What does an optional message in
assertTrue(cond, "text")do?
أظهر الإجابات
- 4.
- At the top line that names your own code.
- Whether the code or the test is wrong, judged against the requirement.
- A fix can break other behavior that was passing (a regression).
- 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
expectedandactual, and that failed assertions throwAssertionFailedError(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
NullPointerExceptionmessage 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.
التطبيق
طبّق هذا الدرس بمختبر الوحدة وتمارينها واختبارها.
تعليم الدرس كمكتمل اختياري ويمكنك التراجع عنه في أي وقت. يبقى تقدمك محفوظًا في هذا المتصفح.