Skip to content

Enhance assertEquals() failure messages with string diffs including c… #4787

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -181,9 +181,93 @@ private static String formatValues(@Nullable Object expected, @Nullable Object a
return "expected: %s but was: %s".formatted(formatClassAndValue(expected, expectedString),
formatClassAndValue(actual, actualString));
}

// Check if both are strings and have whitespace differences
if (expected instanceof String expectedStr && actual instanceof String actualStr) {
String baseMessage = "expected: <%s> but was: <%s>".formatted(expectedString, actualString);
String diff = createWhitespaceDiff(expectedStr, actualStr);
if (diff != null) {
return baseMessage + "\n" + diff;
}
return baseMessage;
}

return "expected: <%s> but was: <%s>".formatted(expectedString, actualString);
}

/**
* Creates a diff showing whitespace differences between two strings.
* Returns null if the strings are identical when whitespace is normalized.
*/
private static @Nullable String createWhitespaceDiff(String expected, String actual) {
// Only show diff if strings differ but have same visible content
if (expected.replaceAll("\\s+", " ").trim().equals(actual.replaceAll("\\s+", " ").trim())) {
return "diff: " + visualizeWhitespace(expected) + "\n" + " " + visualizeWhitespace(actual);
}

// Show diff for any string comparison to help identify whitespace issues
return "diff: " + visualizeWhitespace(expected) + "\n" + " " + visualizeWhitespace(actual);
}

/**
* Converts whitespace characters to their visual representations.
*/
private static String visualizeWhitespace(String str) {
StringBuilder sb = new StringBuilder();
boolean inWhitespace = false;
StringBuilder whitespaceBuffer = new StringBuilder();

for (int i = 0; i < str.length(); i++) {
char c = str.charAt(i);

if (isVisualizableWhitespace(c)) {
if (!inWhitespace) {
inWhitespace = true;
whitespaceBuffer.setLength(0);
whitespaceBuffer.append("[");
}
whitespaceBuffer.append(getWhitespaceRepresentation(c));
}
else {
if (inWhitespace) {
whitespaceBuffer.append("]");
sb.append(whitespaceBuffer.toString());
inWhitespace = false;
}
sb.append(c);
}
}

// Handle case where string ends with whitespace
if (inWhitespace) {
whitespaceBuffer.append("]");
sb.append(whitespaceBuffer.toString());
}

return sb.toString();
}

/**
* Checks if a character should be visualized as whitespace
*/
private static boolean isVisualizableWhitespace(char c) {
return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f' || (Character.isWhitespace(c) && c != ' ');
}

/**
* Gets the string representation of a whitespace character
*/
private static String getWhitespaceRepresentation(char c) {
return switch (c) {
case ' ' -> " ";
case '\t' -> "\\t";
case '\n' -> "\\n";
case '\r' -> "\\r";
case '\f' -> "\\f";
default -> "\\u" + "%04X".formatted((int) c);
};
}

private static String formatClassAndValue(@Nullable Object value, String valueString) {
// If the value is null, return <null> instead of null<null>.
if (value == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

package org.junit.jupiter.api;

import static org.junit.jupiter.api.AssertionTestUtils.assertMessageContains;
import static org.junit.jupiter.api.AssertionTestUtils.assertMessageEndsWith;
import static org.junit.jupiter.api.AssertionTestUtils.assertMessageEquals;
import static org.junit.jupiter.api.AssertionTestUtils.assertMessageStartsWith;
Expand Down Expand Up @@ -1942,7 +1943,7 @@ void assertArrayEqualsDifferentNestedObjectArraysAndMessage() {
}
catch (AssertionFailedError ex) {
assertMessageStartsWith(ex, "message");
assertMessageEndsWith(ex, "array contents differ at index [3][3][0], expected: <2> but was: <99>");
assertMessageContains(ex, "array contents differ at index [3][3][0], expected: <2> but was: <99>");
}

try {
Expand All @@ -1952,7 +1953,7 @@ void assertArrayEqualsDifferentNestedObjectArraysAndMessage() {
}
catch (AssertionFailedError ex) {
assertMessageStartsWith(ex, "message");
assertMessageEndsWith(ex, "array contents differ at index [3][3][0], expected: <2> but was: <99>");
assertMessageContains(ex, "array contents differ at index [3][3][0], expected: <2> but was: <99>");
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
package org.junit.jupiter.api;

import static org.junit.jupiter.api.AssertionTestUtils.assertExpectedAndActualValues;
import static org.junit.jupiter.api.AssertionTestUtils.assertMessageContains;
import static org.junit.jupiter.api.AssertionTestUtils.assertMessageEndsWith;
import static org.junit.jupiter.api.AssertionTestUtils.assertMessageEquals;
import static org.junit.jupiter.api.AssertionTestUtils.assertMessageStartsWith;
Expand Down Expand Up @@ -51,6 +52,21 @@ void assertEqualsByteWithUnequalValues() {
}
}

@Test
void assertEqualsStringWithUnequalSpaceTabs() {
String expected = "a c";
String actual = "a c";
try {
assertEquals(expected, actual, "message");
expectAssertionFailedError();
}
catch (AssertionFailedError ex) {
assertMessageStartsWith(ex, "message");
assertMessageContains(ex, "expected: <a c> but was: <a c>");
assertMessageContains(ex, "diff: a[\\t\\t\\t]c");
}
}

@Test
void assertEqualsByteWithUnequalValuesAndMessage() {
byte expected = 1;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

package org.junit.jupiter.api;

import static org.junit.jupiter.api.AssertionTestUtils.assertMessageContains;
import static org.junit.jupiter.api.AssertionTestUtils.assertMessageEndsWith;
import static org.junit.jupiter.api.AssertionTestUtils.assertMessageEquals;
import static org.junit.jupiter.api.AssertionTestUtils.assertMessageStartsWith;
Expand Down Expand Up @@ -394,7 +395,7 @@ void assertIterableEqualsDifferentNestedIterablesAndMessage() {
}
catch (AssertionFailedError ex) {
assertMessageStartsWith(ex, "message");
assertMessageEndsWith(ex, "iterable contents differ at index [3][3][0], expected: <2> but was: <99>");
assertMessageContains(ex, "iterable contents differ at index [3][3][0], expected: <2> but was: <99>");
}

try {
Expand All @@ -404,7 +405,7 @@ void assertIterableEqualsDifferentNestedIterablesAndMessage() {
}
catch (AssertionFailedError ex) {
assertMessageStartsWith(ex, "message");
assertMessageEndsWith(ex, "iterable contents differ at index [3][3][0], expected: <2> but was: <99>");
assertMessageContains(ex, "iterable contents differ at index [3][3][0], expected: <2> but was: <99>");
}
}

Expand Down