Skip to content

Enhance Assert.AreEqual string comparison to show first difference with index and context #6163

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 16 commits into
base: main
Choose a base branch
from

Conversation

Copilot
Copy link
Contributor

@Copilot Copilot AI commented Jul 25, 2025

MSTest's Assert.AreEqual previously provided minimal information when string comparisons failed, making it difficult for developers to quickly identify the differences. This enhancement brings MSTest's string comparison messages in line with NUnit and XUnit by showing the exact location and context of differences.

Before

Assert.AreEqual failed. Expected:<aaaa>. Actual:<aaab>.

After

String lengths are both 4. Strings differ at index 3. Expected: "aaaa" But was: "aaab" ---^

Key Features

  • Difference Index: Shows exactly where the first difference occurs
  • Length Information: Compares string lengths when they differ
  • Contextual Preview: For long strings, shows relevant context around the difference
  • ASCII-Only Output: Uses ASCII characters to avoid UTF-8 console issues (addresses Set encoding in console to UTF8 #2491)
  • Special Character Handling: Escapes non-printable characters (\t, \n, \r, \0, \uXXXX)
  • Caret Pointer: Visual indicator showing exactly where the difference is

Examples

Basic difference:

Assert.AreEqual("aaaa", "aaab");
// String lengths are both 4. Strings differ at index 3. Expected: "aaaa" But was: "aaab" ---^

Different lengths:

Assert.AreEqual("aaaa", "aaaab");
// Expected string length 4 but was 5. Strings differ at index 4. Expected: "aaaa" But was: "aaaab" ----^

Special characters:

Assert.AreEqual("aa\ta", "aa a");
// String lengths are both 4. Strings differ at index 2. Expected: "aa\ta" But was: "aa a" --^

Long strings (shows contextual preview):

string expected = new string('a', 50) + "b" + new string('c', 50);
string actual = new string('a', 50) + "d" + new string('c', 50);
Assert.AreEqual(expected, actual);
// String lengths are both 101. Strings differ at index 50. Expected: "aaaaaaaaaabccccccccc" But was: "aaaaaaaaaadccccccccc" ----------^

Implementation Details

  • Enhanced ThrowAssertAreEqualFailed to detect string types and apply special formatting
  • Added helper methods for finding differences and creating contextual previews
  • Extended enhancement to culture-aware string assertions for consistent experience
  • Added comprehensive test coverage for all scenarios
  • Updated all XLF localization files with new message format

The enhancement only affects string comparison failures and maintains backward compatibility for all other assertion types.

Fixes #4261 and fixes #978.


💬 Share your feedback on Copilot coding agent for the chance to win a $200 gift card! Click here to start the survey.

@Copilot Copilot AI changed the title [WIP] Assert on string should show at least the first difference. Enhance Assert.AreEqual string comparison to show first difference with index and context Jul 25, 2025
@Copilot Copilot AI requested a review from Evangelink July 25, 2025 08:42
Copilot finished work on behalf of Evangelink July 25, 2025 08:42
Copy link
Member

@Evangelink Evangelink left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's have expected and actual on new lines. Also add an example of a diff that appears far in a long string (e.g. position 90 of a 200 char string) to confirm we do not display all 90 char but use ellipsis to cut long before and after

Copy link
Contributor Author

Copilot AI commented Jul 25, 2025

Let's have expected and actual on new lines. Also add an example of a diff that appears far in a long string (e.g. position 90 of a 200 char string) to confirm we do not display all 90 char but use el...

I've implemented both requested changes:

  1. Newline format: Updated the message format to put expected and actual values on separate lines:

    String lengths are both 4. Strings differ at index 3.
    Expected: "aaaa"
    But was:  "aaab"
    ---^
    
  2. Long string ellipsis test: Added AreEqualLongStringWithDifferenceFarFromStart() that tests a 200-character string with difference at position 90. The implementation now shows ellipsis (...) when content is truncated, and the test verifies we don't display excessive leading context.

Changes in commit bf7af04:

  • Updated resource files and all XLF localizations
  • Enhanced ellipsis handling in CreateStringPreview
  • Adjusted caret positioning to account for ellipsis
  • Added comprehensive test coverage

The ellipsis properly truncates long strings while maintaining readability around the difference point.

@Copilot Copilot AI requested a review from Evangelink July 25, 2025 09:31
Copilot finished work on behalf of Evangelink July 25, 2025 09:31
Copy link
Member

@Evangelink Evangelink left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ensure all newly added tests assert full message and not only contains

Copy link
Contributor Author

Copilot AI commented Jul 25, 2025

Ensure all newly added tests assert full message and not only contains

Updated all newly added string comparison tests to assert the full exact message instead of using .Contains(). The tests now validate the complete expected output format including length info, difference index, expected/actual values with proper escaping, and caret positioning.

Addressed in commit aaa23dc.

@Copilot Copilot AI requested a review from Evangelink July 25, 2025 11:09
Copilot finished work on behalf of Evangelink July 25, 2025 11:09
@Copilot Copilot AI requested a review from Evangelink July 25, 2025 11:35
Copilot finished work on behalf of Evangelink July 25, 2025 11:35
@Evangelink
Copy link
Member

Ensure that all added tests are written using FluentAssertions (don't add package)

@Copilot Copilot AI requested a review from Evangelink July 25, 2025 14:55
Copilot finished work on behalf of Evangelink July 25, 2025 14:55
Copy link
Contributor Author

Copilot AI commented Jul 25, 2025

Ensure that all added tests are written using FluentAssertions (don't add package)

Converted all newly added string comparison tests to use FluentAssertions syntax. Added using FluentAssertions; and changed Verify(ex.Message == ...) to ex.Message.Should().Be(...) in all 5 new test methods. Commit 17ef3cf.

Copilot finished work on behalf of Evangelink July 25, 2025 15:06
@Evangelink Evangelink marked this pull request as ready for review July 25, 2025 16:54
@Evangelink Evangelink enabled auto-merge (squash) July 25, 2025 17:16
Copy link
Member

@nohwnd nohwnd left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logic here should probably be changed quite a bit, to think about the shortening of the string as "masking" it with 3 dots where we have 4 or more characters to render. Otherwise it will have different visual bugs.

I would avoid replacing the utf chars, I want my smileys to be rendered for me normally.

Other small fixes should be applied to maximize the info user sees in cases where the diff is near the end of the string.

Comment on lines +879 to +890
result.Append(CultureInfo.InvariantCulture, $"\\u{(int)c:X4}");
#else
result.Append($"\\u{(int)c:X4}");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here we add more chars to the string but we already figured out the diference so the arrow will point at earlier character:

Exception ex = VerifyThrows(() => Assert.AreEqual("🤮aaa", "🤮aab"));

Assert.AreEqual failed. String lengths are both 5 but differ at index 4.
Expected: "\uD83E\uDD2Eaaa"
But was:  "\uD83E\uDD2Eaab"
---------------^

Also replacing the characters with their unicode counterparts seems like we are giving up convenience for supporting the worst case scenario, I would not do that.

This case also highlights that the code is counting byte length (string.lenght) and not the text element size, which might align better with the user expectation.

In either case if we manipulate the string we should make sure that the difference counting reflects that.

To me I'd keep the string as is. replace the non-printable chars at the beginning with their printable counterparts e.g \e to and leave that as is.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed the caret positioning issue by tracking character replacements in CreateStringPreviewWithCaretInfo(). Now the caret correctly points to the actual difference position even when characters are escaped. Also changed to preserve Unicode characters (like emojis) rather than escaping them unnecessarily. Commit bc3be4c.

Exception ex = VerifyThrows(() => Assert.AreEqual("aa\ta", "aa a"));
ex.Message.Should().Be("""
Assert.AreEqual failed. String lengths are both 4 but differ at index 2.
Expected: "aa\ta"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Expected: "aa\ta"
Expected: "aa\\ta"

If the intent was to show \t and not tab char.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed the test expectation to properly show escaped tab character as "aa\\ta" to match what the implementation actually produces when displaying the \t escape sequence. Commit bc3be4c.

Copy link
Contributor Author

Copilot AI commented Jul 28, 2025

The logic here should probably be changed quite a bit, to think about the shortening of the string as "masking" it with 3 dots where we have 4 or more characters to render. Otherwise it will have diff...

I've implemented the suggested improvements in commit bc3be4c:

  1. Better ellipsis masking: Now only adds "..." when masking 4+ characters, avoiding misleading visual length
  2. Preserve Unicode characters: Emojis and other Unicode characters are kept as-is, only control characters are escaped
  3. Improved context positioning: Uses 75% positioning within the diff window to show more relevant context, especially helpful for JSON and repetitive data
  4. Better short string handling: Shows full content for shorter strings without unnecessary truncation

The logic now maximizes the information shown to users while addressing the visual bugs mentioned.

@Copilot Copilot AI requested a review from nohwnd July 28, 2025 10:35
Copilot finished work on behalf of nohwnd July 28, 2025 10:35
Copilot finished work on behalf of nohwnd July 28, 2025 10:43
string expected, string actual, int diffIndex, int contextLength)
{
int maxLength = Math.Max(expected.Length, actual.Length);

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

src/TestFramework/TestFramework/Assertions/Assert.AreEqual.cs(804,1): error IDE0055: (NETCORE_ENGINEERING_TELEMETRY=Build) Fix formatting (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0055)

Copy link
Contributor Author

Copilot AI commented Jul 28, 2025

Ensure that all added tests are written using FluentAssertions (don't add package)

Converted all newly added string comparison tests to use FluentAssertions syntax. Changed Verify(ex.Message == ...) to ex.Message.Should().Be(...) in all 5 new test methods. Commit 17ef3cf.

@Copilot Copilot AI requested a review from nohwnd July 28, 2025 15:36
Copilot finished work on behalf of nohwnd July 28, 2025 15:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Assert on string should show at least the first difference. Idea: Alternative output formatting for Assert.AreEqual(string, string)
3 participants