diff --git a/docs/RFCs/012-Structured-Assertion-Messages.md b/docs/RFCs/012-Structured-Assertion-Messages.md index 43a3bfdccb..3e3c898a98 100644 --- a/docs/RFCs/012-Structured-Assertion-Messages.md +++ b/docs/RFCs/012-Structured-Assertion-Messages.md @@ -72,7 +72,7 @@ Examples: | Assertion | Line 1 | | --------- | ------ | | `AreEqual` (int) | `Assertion failed. Expected values to be equal.` | -| `AreEqual` (string) | `Assertion failed. Expected strings to be equal (case-sensitive).` | +| `AreEqual` (string) | `Assertion failed. Expected strings to be equal.` | | `IsTrue` | `Assertion failed. Expected condition to be true.` | | `IsNull` | `Assertion failed. Expected value to be null.` | | `IsInstanceOfType` | `Assertion failed. Expected value to be of type String (or derived).` | @@ -270,7 +270,7 @@ Assert.AreEqual(expectedCount, actualCount) ### Assert.AreEqual (strings, with user message) ```text -Assertion failed. Expected strings to be equal (case-sensitive). +Assertion failed. Expected strings to be equal. Strings have same length (11) but differ at 1 location(s). First difference at index 7. The greeting should include the user's full name @@ -317,7 +317,7 @@ Assert.ThrowsExactly(() => Validate(input)) ### Assert.AreEqual (large strings) ```text -Assertion failed. Expected strings to be equal (case-sensitive). +Assertion failed. Expected strings to be equal. Strings have different lengths (expected: 50000, actual: 49997) and differ at 1 location(s). First difference at index 1042. expected: @@ -382,7 +382,7 @@ Assert.AreEqual( Output — the multiline expression is replaced with `` in the call-site: ```text -Assertion failed. Expected strings to be equal (case-sensitive). +Assertion failed. Expected strings to be equal. Strings differ at 1 location(s). First difference at index 22. expected: "{\n \"name\": \"Alice\",\n \"age\": 30\n}" @@ -429,7 +429,7 @@ expected: 42 actual: 37 ``` -Note: When the generic `AreEqual` overload is called with `T = string` (without `ignoreCase`/`culture` parameters), the message **auto-detects the string type** and uses the string-specific format (`"Expected strings to be equal (case-sensitive)."`) with full string diff diagnostics. The generic overload defaults to case-sensitive ordinal comparison, which is exactly what the string-specific format conveys. Developers writing `Assert.AreEqual("expected", actual)` get string diagnostics without needing to know about the string-specific overload. +Note: When the generic `AreEqual` overload is called with `T = string` (without `ignoreCase`/`culture` parameters), the message **auto-detects the string type** and uses the string-specific format (`"Expected strings to be equal."`) with full string diff diagnostics. The generic overload defaults to ordinal equality (`EqualityComparer.Default`), which is case-sensitive. Developers writing `Assert.AreEqual("expected", actual)` get string diagnostics without needing to know about the string-specific overload. #### Assert.AreEqual (with delta) @@ -443,20 +443,20 @@ delta: 0.001 Note: The `delta` overload exists for `float`, `double`, `decimal`, and `long`. All four types use the same message format. The rendered precision follows the type’s default `ToString()` formatting. -#### Assert.AreEqual (string, case-sensitive) +#### Assert.AreEqual (string) ```text -Assertion failed. Expected strings to be equal (case-sensitive). +Assertion failed. Expected strings to be equal. Strings have same length (11) but differ at 1 location(s). First difference at index 7. expected: "hello world" actual: "hello wrold" ``` -#### Assert.AreEqual (string, case-insensitive with culture) +#### Assert.AreEqual (string, with ignoreCase and culture) ```text -Assertion failed. Expected strings to be equal (case-insensitive). +Assertion failed. Expected strings to be equal. Strings have different lengths (expected: 6, actual: 8) and differ at 1 location(s). First difference at index 6. expected: "straße" @@ -465,7 +465,7 @@ ignore case: true culture: de-DE ``` -Note: Under `de-DE` culture with case-insensitive comparison, `"straße"` and `"STRASSE"` are considered equal (ß expands to SS). The example above shows a genuinely failing comparison where the actual string has additional content beyond the case-equivalent portion. +Note: Under `de-DE` culture with case-insensitive comparison, `"straße"` and `"STRASSE"` are considered equal (ß expands to SS). The example above shows a genuinely failing comparison where the actual string has additional content beyond the case-equivalent portion. The structured-message format itself does not change between case-sensitive and case-insensitive overloads — the summary line is the same and the `ignore case:` / `culture:` evidence lines are added only when those parameters are supplied. #### Assert.AreNotEqual (generic) diff --git a/src/TestFramework/TestFramework/Assertions/Assert.AreEqual.InterpolatedStringHandlers.cs b/src/TestFramework/TestFramework/Assertions/Assert.AreEqual.InterpolatedStringHandlers.cs index 49db337dde..a887cc5fac 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.AreEqual.InterpolatedStringHandlers.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.AreEqual.InterpolatedStringHandlers.cs @@ -48,8 +48,7 @@ internal void ComputeAssertion(string expectedExpression, string actualExpressio { if (_builder is not null) { - _builder.Insert(0, string.Format(CultureInfo.CurrentCulture, FrameworkMessages.CallerArgumentExpressionTwoParametersMessage, "expected", expectedExpression, "actual", actualExpression) + " "); - ReportAssertAreEqualFailed(_expected, _actual, _builder.ToString()); + ReportAssertAreEqualFailed(_expected, _actual, _builder.ToString(), expectedExpression, actualExpression); } } @@ -115,8 +114,7 @@ internal void ComputeAssertion(string notExpectedExpression, string actualExpres { if (_builder is not null) { - _builder.Insert(0, string.Format(CultureInfo.CurrentCulture, FrameworkMessages.CallerArgumentExpressionTwoParametersMessage, "notExpected", notExpectedExpression, "actual", actualExpression) + " "); - ReportAssertAreNotEqualFailed(_notExpected, _actual, _builder.ToString()); + ReportAssertAreNotEqualFailed(_notExpected, _actual, _builder.ToString(), notExpectedExpression, actualExpression); } } @@ -265,7 +263,7 @@ internal void ComputeAssertion(string expectedExpression, string actualExpressio public readonly struct AssertNonGenericAreNotEqualInterpolatedStringHandler { private readonly StringBuilder? _builder; - private readonly Action? _failAction; + private readonly Action? _failAction; public AssertNonGenericAreNotEqualInterpolatedStringHandler(int literalLength, int formattedCount, float notExpected, float actual, float delta, out bool shouldAppend) { @@ -273,7 +271,8 @@ public AssertNonGenericAreNotEqualInterpolatedStringHandler(int literalLength, i if (shouldAppend) { _builder = new StringBuilder(literalLength + formattedCount); - _failAction = userMessage => ReportAssertAreNotEqualFailed(notExpected, actual, delta, userMessage); + _failAction = (userMessage, notExpectedExpr, actualExpr) => + ReportAssertAreNotEqualFailed(notExpected, actual, delta, BuildUserMessageForNotExpectedExpressionAndActualExpression(userMessage, notExpectedExpr, actualExpr)); } } @@ -283,7 +282,8 @@ public AssertNonGenericAreNotEqualInterpolatedStringHandler(int literalLength, i if (shouldAppend) { _builder = new StringBuilder(literalLength + formattedCount); - _failAction = userMessage => ReportAssertAreNotEqualFailed(notExpected, actual, delta, userMessage); + _failAction = (userMessage, notExpectedExpr, actualExpr) => + ReportAssertAreNotEqualFailed(notExpected, actual, delta, BuildUserMessageForNotExpectedExpressionAndActualExpression(userMessage, notExpectedExpr, actualExpr)); } } @@ -293,7 +293,8 @@ public AssertNonGenericAreNotEqualInterpolatedStringHandler(int literalLength, i if (shouldAppend) { _builder = new StringBuilder(literalLength + formattedCount); - _failAction = userMessage => ReportAssertAreNotEqualFailed(notExpected, actual, delta, userMessage); + _failAction = (userMessage, notExpectedExpr, actualExpr) => + ReportAssertAreNotEqualFailed(notExpected, actual, delta, BuildUserMessageForNotExpectedExpressionAndActualExpression(userMessage, notExpectedExpr, actualExpr)); } } @@ -303,7 +304,8 @@ public AssertNonGenericAreNotEqualInterpolatedStringHandler(int literalLength, i if (shouldAppend) { _builder = new StringBuilder(literalLength + formattedCount); - _failAction = userMessage => ReportAssertAreNotEqualFailed(notExpected, actual, delta, userMessage); + _failAction = (userMessage, notExpectedExpr, actualExpr) => + ReportAssertAreNotEqualFailed(notExpected, actual, delta, BuildUserMessageForNotExpectedExpressionAndActualExpression(userMessage, notExpectedExpr, actualExpr)); } } @@ -319,18 +321,12 @@ public AssertNonGenericAreNotEqualInterpolatedStringHandler(int literalLength, i if (shouldAppend) { _builder = new StringBuilder(literalLength + formattedCount); - _failAction = userMessage => ReportAssertAreNotEqualFailed(notExpected, actual, userMessage); + _failAction = (userMessage, notExpectedExpr, actualExpr) => ReportAssertAreNotEqualFailed(notExpected, actual, userMessage, notExpectedExpr, actualExpr); } } internal void ComputeAssertion(string notExpectedExpression, string actualExpression) - { - if (_failAction is not null) - { - _builder!.Insert(0, string.Format(CultureInfo.CurrentCulture, FrameworkMessages.CallerArgumentExpressionTwoParametersMessage, "notExpected", notExpectedExpression, "actual", actualExpression) + " "); - _failAction.Invoke(_builder!.ToString()); - } - } + => _failAction?.Invoke(_builder!.ToString(), notExpectedExpression, actualExpression); public void AppendLiteral(string value) => _builder!.Append(value); diff --git a/src/TestFramework/TestFramework/Assertions/Assert.AreEqual.String.cs b/src/TestFramework/TestFramework/Assertions/Assert.AreEqual.String.cs index 74299f9e38..2fa3024a36 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.AreEqual.String.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.AreEqual.String.cs @@ -241,8 +241,7 @@ public static void AreNotEqual(string? notExpected, string? actual, bool ignoreC return; } - string userMessage = BuildUserMessageForNotExpectedExpressionAndActualExpression(message, notExpectedExpression, actualExpression); - ReportAssertAreNotEqualFailed(notExpected, actual, userMessage); + ReportAssertAreNotEqualFailed(notExpected, actual, message, notExpectedExpression, actualExpression); } #pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters diff --git a/src/TestFramework/TestFramework/Assertions/Assert.AreEqual.cs b/src/TestFramework/TestFramework/Assertions/Assert.AreEqual.cs index 59710dcab7..56a1a18b02 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.AreEqual.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.AreEqual.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. namespace Microsoft.VisualStudio.TestTools.UnitTesting; @@ -139,8 +139,7 @@ public static void AreEqual(T? expected, T? actual, IEqualityComparer comp return; } - string userMessage = BuildUserMessageForExpectedExpressionAndActualExpression(message, expectedExpression, actualExpression); - ReportAssertAreEqualFailed(expected, actual, userMessage); + ReportAssertAreEqualFailed(expected, actual, message, expectedExpression, actualExpression); } private static bool AreEqualFailing(T? expected, T? actual, IEqualityComparer? comparer) @@ -245,26 +244,58 @@ private static string FormatStringDifferenceMessage(string expected, string actu } [DoesNotReturn] - private static void ReportAssertAreEqualFailed(object? expected, object? actual, string userMessage) + private static void ReportAssertAreEqualFailed(object? expected, object? actual, string? message, string expectedExpression, string actualExpression) { - string finalMessage = actual != null && expected != null && !actual.GetType().Equals(expected.GetType()) - ? string.Format( - CultureInfo.CurrentCulture, - FrameworkMessages.AreEqualDifferentTypesFailMsg, - userMessage, - ReplaceNulls(expected), - expected.GetType().FullName, - ReplaceNulls(actual), - actual.GetType().FullName) - : expected is string expectedString && actual is string actualString - ? FormatStringComparisonMessage(expectedString, actualString, userMessage) - : string.Format( - CultureInfo.CurrentCulture, - FrameworkMessages.AreEqualFailMsg, - userMessage, - ReplaceNulls(expected), - ReplaceNulls(actual)); - ReportAssertFailed("Assert.AreEqual", finalMessage); + string expectedRendered = AssertionValueRenderer.RenderValue(expected); + string actualRendered = AssertionValueRenderer.RenderValue(actual); + + string summary; + EvidenceBlock evidence; + string? additionalSummaryLine = null; + + if (actual is not null && expected is not null && !actual.GetType().Equals(expected.GetType())) + { + Type expectedType = expected.GetType(); + Type actualType = actual.GetType(); + summary = FrameworkMessages.AreEqualDifferentTypesFailedSummary; + evidence = EvidenceBlock.Create() + .AddLine("expected:", expectedRendered) + .AddLine("expected type:", expectedType.FullName ?? expectedType.Name) + .AddLine("actual:", actualRendered) + .AddLine("actual type:", actualType.FullName ?? actualType.Name); + } + else if (expected is string expectedString && actual is string actualString) + { + summary = FrameworkMessages.AreEqualStringsFailedSummary; + int diffIndex = FindFirstStringDifference(expectedString, actualString); + additionalSummaryLine = expectedString.Length == actualString.Length + ? string.Format(CultureInfo.CurrentCulture, FrameworkMessages.AreEqualStringDiffLengthBothMsg, expectedString.Length, diffIndex) + : string.Format(CultureInfo.CurrentCulture, FrameworkMessages.AreEqualStringDiffLengthDifferentMsg, expectedString.Length, actualString.Length); + + evidence = EvidenceBlock.Create() + .AddLine("expected:", expectedRendered) + .AddLine("actual:", actualRendered); + } + else + { + summary = FrameworkMessages.AreEqualFailedSummary; + evidence = EvidenceBlock.Create() + .AddLine("expected:", expectedRendered) + .AddLine("actual:", actualRendered); + } + + StructuredAssertionMessage structured = new(summary); + if (additionalSummaryLine is not null) + { + structured.WithAdditionalSummaryLine(additionalSummaryLine); + } + + structured.WithUserMessage(message); + structured.WithEvidence(evidence); + structured.WithExpectedAndActual(expectedRendered, actualRendered); + structured.WithCallSiteExpression(FormatBinaryCallSiteExpression("Assert.AreEqual", expectedExpression, "expected", actualExpression, "actual")); + + ReportAssertFailed(structured); } /// @@ -363,23 +394,29 @@ public static void AreNotEqual(T? notExpected, T? actual, IEqualityComparer(T? notExpected, T? actual, IEqualityComparer? comparer) => (comparer ?? EqualityComparer.Default).Equals(notExpected!, actual!); [DoesNotReturn] - private static void ReportAssertAreNotEqualFailed(object? notExpected, object? actual, string userMessage) + private static void ReportAssertAreNotEqualFailed(object? notExpected, object? actual, string? message, string notExpectedExpression, string actualExpression) { - string finalMessage = string.Format( - CultureInfo.CurrentCulture, - FrameworkMessages.AreNotEqualFailMsg, - userMessage, - ReplaceNulls(notExpected), - ReplaceNulls(actual)); - ReportAssertFailed("Assert.AreNotEqual", finalMessage); + string notExpectedRendered = AssertionValueRenderer.RenderValue(notExpected); + string actualRendered = AssertionValueRenderer.RenderValue(actual); + + EvidenceBlock evidence = EvidenceBlock.Create() + .AddLine("notExpected:", notExpectedRendered) + .AddLine("actual:", actualRendered); + + StructuredAssertionMessage structured = new(FrameworkMessages.AreNotEqualFailedSummary); + structured.WithUserMessage(message); + structured.WithEvidence(evidence); + structured.WithExpectedAndActual($"not {notExpectedRendered}", actualRendered); + structured.WithCallSiteExpression(FormatBinaryCallSiteExpression("Assert.AreNotEqual", notExpectedExpression, "notExpected", actualExpression, "actual")); + + ReportAssertFailed(structured); } #pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters diff --git a/src/TestFramework/TestFramework/Assertions/Assert.cs b/src/TestFramework/TestFramework/Assertions/Assert.cs index b3c20de7f0..fa3a3dc428 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.cs @@ -314,6 +314,26 @@ internal static string ReplaceNulls(object? input) ? null : $"{methodName}({expression})"; + private static readonly char[] LineBreakChars = ['\n', '\r']; + + /// + /// Formats a call-site expression like Assert.MethodName(expression1, expression2). + /// When either expression is empty the call-site is omitted. When an expression contains + /// newlines (multiline constant) it is replaced with a <paramName> placeholder. + /// + private static string? FormatBinaryCallSiteExpression(string methodName, string expression1, string paramName1, string expression2, string paramName2) + { + if (string.IsNullOrWhiteSpace(expression1) || string.IsNullOrWhiteSpace(expression2)) + { + return null; + } + + string arg1 = expression1.IndexOfAny(LineBreakChars) >= 0 ? $"<{paramName1}>" : expression1; + string arg2 = expression2.IndexOfAny(LineBreakChars) >= 0 ? $"<{paramName2}>" : expression2; + + return $"{methodName}({arg1}, {arg2})"; + } + private static int CompareInternal(string? expected, string? actual, bool ignoreCase, CultureInfo culture) #pragma warning disable CA1309 // Use ordinal string comparison => string.Compare(expected, actual, ignoreCase, culture); diff --git a/src/TestFramework/TestFramework/Resources/FrameworkMessages.resx b/src/TestFramework/TestFramework/Resources/FrameworkMessages.resx index dd9fc1b561..b4f1dc13fb 100644 --- a/src/TestFramework/TestFramework/Resources/FrameworkMessages.resx +++ b/src/TestFramework/TestFramework/Resources/FrameworkMessages.resx @@ -479,6 +479,18 @@ Actual: {2} Expected value to not be null. + + Expected values to be equal. + + + Expected values to be equal, but they are of different types. + + + Expected strings to be equal. + + + Expected values to not be equal. + Expected string to match the specified regular expression. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.cs.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.cs.xlf index 59ccc4447e..6dfa00993c 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.cs.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.cs.xlf @@ -542,6 +542,26 @@ Skutečnost: {2} Expected value to not be null. + + Expected values to not be equal. + Expected values to not be equal. + + + + Expected strings to be equal. + Expected strings to be equal. + + + + Expected values to be equal, but they are of different types. + Expected values to be equal, but they are of different types. + + + + Expected values to be equal. + Expected values to be equal. + + Expected value to be null. Expected value to be null. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.de.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.de.xlf index 512defa5c8..a94003ad6c 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.de.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.de.xlf @@ -542,6 +542,26 @@ Tatsächlich: {2} Expected value to not be null. + + Expected values to not be equal. + Expected values to not be equal. + + + + Expected strings to be equal. + Expected strings to be equal. + + + + Expected values to be equal, but they are of different types. + Expected values to be equal, but they are of different types. + + + + Expected values to be equal. + Expected values to be equal. + + Expected value to be null. Expected value to be null. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.es.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.es.xlf index 0e8a5863fd..3def3a2ec9 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.es.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.es.xlf @@ -542,6 +542,26 @@ Real: {2} Se esperaba que el valor no fuera null. + + Expected values to not be equal. + Expected values to not be equal. + + + + Expected strings to be equal. + Expected strings to be equal. + + + + Expected values to be equal, but they are of different types. + Expected values to be equal, but they are of different types. + + + + Expected values to be equal. + Expected values to be equal. + + Expected value to be null. Se esperaba que el valor fuera null. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.fr.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.fr.xlf index f2393eb9c6..1ea1f89ad8 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.fr.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.fr.xlf @@ -542,6 +542,26 @@ Réel : {2} Expected value to not be null. + + Expected values to not be equal. + Expected values to not be equal. + + + + Expected strings to be equal. + Expected strings to be equal. + + + + Expected values to be equal, but they are of different types. + Expected values to be equal, but they are of different types. + + + + Expected values to be equal. + Expected values to be equal. + + Expected value to be null. Expected value to be null. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.it.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.it.xlf index 561a73665e..02ad09c7ae 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.it.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.it.xlf @@ -542,6 +542,26 @@ Effettivo: {2} Expected value to not be null. + + Expected values to not be equal. + Expected values to not be equal. + + + + Expected strings to be equal. + Expected strings to be equal. + + + + Expected values to be equal, but they are of different types. + Expected values to be equal, but they are of different types. + + + + Expected values to be equal. + Expected values to be equal. + + Expected value to be null. Expected value to be null. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ja.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ja.xlf index 16e474fec9..812edff46d 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ja.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ja.xlf @@ -542,6 +542,26 @@ Actual: {2} 予期される値は null ではありません。 + + Expected values to not be equal. + Expected values to not be equal. + + + + Expected strings to be equal. + Expected strings to be equal. + + + + Expected values to be equal, but they are of different types. + Expected values to be equal, but they are of different types. + + + + Expected values to be equal. + Expected values to be equal. + + Expected value to be null. 予期される値は null です。 diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ko.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ko.xlf index af329e6b4f..72bba587b3 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ko.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ko.xlf @@ -542,6 +542,26 @@ Actual: {2} Expected value to not be null. + + Expected values to not be equal. + Expected values to not be equal. + + + + Expected strings to be equal. + Expected strings to be equal. + + + + Expected values to be equal, but they are of different types. + Expected values to be equal, but they are of different types. + + + + Expected values to be equal. + Expected values to be equal. + + Expected value to be null. Expected value to be null. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pl.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pl.xlf index 2400f343a5..c758eae0de 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pl.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pl.xlf @@ -542,6 +542,26 @@ Rzeczywiste: {2} Expected value to not be null. + + Expected values to not be equal. + Expected values to not be equal. + + + + Expected strings to be equal. + Expected strings to be equal. + + + + Expected values to be equal, but they are of different types. + Expected values to be equal, but they are of different types. + + + + Expected values to be equal. + Expected values to be equal. + + Expected value to be null. Expected value to be null. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pt-BR.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pt-BR.xlf index f154240002..b2260d80c1 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pt-BR.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pt-BR.xlf @@ -542,6 +542,26 @@ Real: {2} Expected value to not be null. + + Expected values to not be equal. + Expected values to not be equal. + + + + Expected strings to be equal. + Expected strings to be equal. + + + + Expected values to be equal, but they are of different types. + Expected values to be equal, but they are of different types. + + + + Expected values to be equal. + Expected values to be equal. + + Expected value to be null. Expected value to be null. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ru.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ru.xlf index 723aab1646..c5c90d2217 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ru.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ru.xlf @@ -542,6 +542,26 @@ Actual: {2} Expected value to not be null. + + Expected values to not be equal. + Expected values to not be equal. + + + + Expected strings to be equal. + Expected strings to be equal. + + + + Expected values to be equal, but they are of different types. + Expected values to be equal, but they are of different types. + + + + Expected values to be equal. + Expected values to be equal. + + Expected value to be null. Expected value to be null. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.tr.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.tr.xlf index 207520dbec..9fcd48339f 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.tr.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.tr.xlf @@ -542,6 +542,26 @@ Gerçekte olan: {2} Beklenen değer null olmamalı. + + Expected values to not be equal. + Expected values to not be equal. + + + + Expected strings to be equal. + Expected strings to be equal. + + + + Expected values to be equal, but they are of different types. + Expected values to be equal, but they are of different types. + + + + Expected values to be equal. + Expected values to be equal. + + Expected value to be null. Beklenen değer null olmalı. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hans.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hans.xlf index 89894d5145..8b8d275b83 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hans.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hans.xlf @@ -542,6 +542,26 @@ Actual: {2} Expected value to not be null. + + Expected values to not be equal. + Expected values to not be equal. + + + + Expected strings to be equal. + Expected strings to be equal. + + + + Expected values to be equal, but they are of different types. + Expected values to be equal, but they are of different types. + + + + Expected values to be equal. + Expected values to be equal. + + Expected value to be null. Expected value to be null. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hant.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hant.xlf index ae7770aaeb..e86a09e394 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hant.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hant.xlf @@ -542,6 +542,26 @@ Actual: {2} Expected value to not be null. + + Expected values to not be equal. + Expected values to not be equal. + + + + Expected strings to be equal. + Expected strings to be equal. + + + + Expected values to be equal, but they are of different types. + Expected values to be equal, but they are of different types. + + + + Expected values to be equal. + Expected values to be equal. + + Expected value to be null. Expected value to be null. diff --git a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/OutputTests.cs b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/OutputTests.cs index 762ba5d867..87fb0846a9 100644 --- a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/OutputTests.cs +++ b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/OutputTests.cs @@ -17,7 +17,8 @@ public async Task DetailedOutputIsAsExpected(string tfm) TestHostResult testHostResult = await testHost.ExecuteAsync("--output detailed", cancellationToken: TestContext.CancellationToken); // Assert - testHostResult.AssertOutputContains("Assert.AreEqual failed. Expected:<1>. Actual:<2>."); + testHostResult.AssertOutputContains("Assertion failed. Expected values to be equal."); + testHostResult.AssertOutputContains("Assert.AreEqual(1, 2)"); testHostResult.AssertOutputContains(""" Standard output Console message diff --git a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.AreEqualTests.cs b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.AreEqualTests.cs index f7901cfdce..213fc4b2a7 100644 --- a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.AreEqualTests.cs +++ b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.AreEqualTests.cs @@ -302,7 +302,78 @@ public void AreEqualTwoObjectsDifferentTypeShouldFail() { Action action = () => Assert.AreEqual(new object(), 1); action.Should().Throw() - .And.Message.Should().Contain("Assert.AreEqual failed. Expected:. Actual:<1 (System.Int32)>."); + .Which.Message.Should().Be( + """ + Assertion failed. Expected values to be equal, but they are of different types. + + expected: System.Object + expected type: System.Object + actual: 1 + actual type: System.Int32 + + Assert.AreEqual(new object(), 1) + """); + } + + public void AreNotEqual_PopulatesExpectedAndActualTextWithNotPrefix() + { + Action action = () => Assert.AreNotEqual(0, 0); + AssertFailedException ex = action.Should().Throw().Which; + ex.ExpectedText.Should().Be("not 0"); + ex.ActualText.Should().Be("0"); + ex.Data["assert.expected"].Should().Be("not 0"); + ex.Data["assert.actual"].Should().Be("0"); + } + + public void AreNotEqual_FailsWithStructuredMessage() + { + Action action = () => Assert.AreNotEqual(0, 0); + action.Should().Throw() + .Which.Message.Should().Be( + """ + Assertion failed. Expected values to not be equal. + + notExpected: 0 + actual: 0 + + Assert.AreNotEqual(0, 0) + """); + } + + public void AreEqual_MultilineExpectedExpression_UsesPlaceholderInCallSite() + { + Action action = () => Assert.AreEqual( + """ + line1 + line2 + """, + "different"); + action.Should().Throw() + .Which.Message.Should().EndWith("Assert.AreEqual(, \"different\")"); + } + + public void AreEqual_MultilineActualExpression_UsesPlaceholderInCallSite() + { + Action action = () => Assert.AreEqual( + "different", + """ + line1 + line2 + """); + action.Should().Throw() + .Which.Message.Should().EndWith("Assert.AreEqual(\"different\", )"); + } + + public void AreNotEqual_MultilineNotExpectedExpression_UsesPlaceholderInCallSite() + { + string value = "x"; + Action action = () => Assert.AreNotEqual( + """ + x + """, + value); + action.Should().Throw() + .Which.Message.Should().EndWith("Assert.AreNotEqual(, value)"); } public void AreEqualWithTypeOverridingEqualsShouldWork() @@ -373,7 +444,16 @@ public async Task GenericAreEqual_InterpolatedString_DifferentValues_ShouldFail( DateTime dateTime = DateTime.Now; Func action = async () => Assert.AreEqual(0, 1, $"User-provided message. {o}, {o,35}, {await GetHelloStringAsync()}, {new DummyIFormattable()}, {dateTime:tt}, {dateTime,5:tt}"); (await action.Should().ThrowAsync()) - .WithMessage($"Assert.AreEqual failed. Expected:<0>. Actual:<1>. 'expected' expression: '0', 'actual' expression: '1'. User-provided message. DummyClassTrackingToStringCalls, DummyClassTrackingToStringCalls, Hello, DummyIFormattable.ToString(), {string.Format(null, "{0:tt}", dateTime)}, {string.Format(null, "{0,5:tt}", dateTime)}"); + .Which.Message.Should().Be( + $""" + Assertion failed. Expected values to be equal. + User-provided message. DummyClassTrackingToStringCalls, DummyClassTrackingToStringCalls, Hello, DummyIFormattable.ToString(), {string.Format(null, "{0:tt}", dateTime)}, {string.Format(null, "{0,5:tt}", dateTime)} + + expected: 0 + actual: 1 + + Assert.AreEqual(0, 1) + """); o.WasToStringCalled.Should().BeTrue(); } @@ -390,7 +470,16 @@ public async Task GenericAreNotEqual_InterpolatedString_SameValues_ShouldFail() DateTime dateTime = DateTime.Now; Func action = async () => Assert.AreNotEqual(0, 0, $"User-provided message. {o}, {o,35}, {await GetHelloStringAsync()}, {new DummyIFormattable()}, {dateTime:tt}, {dateTime,5:tt}"); (await action.Should().ThrowAsync()) - .WithMessage($"Assert.AreNotEqual failed. Expected any value except:<0>. Actual:<0>. 'notExpected' expression: '0', 'actual' expression: '0'. User-provided message. DummyClassTrackingToStringCalls, DummyClassTrackingToStringCalls, Hello, DummyIFormattable.ToString(), {string.Format(null, "{0:tt}", dateTime)}, {string.Format(null, "{0,5:tt}", dateTime)}"); + .Which.Message.Should().Be( + $""" + Assertion failed. Expected values to not be equal. + User-provided message. DummyClassTrackingToStringCalls, DummyClassTrackingToStringCalls, Hello, DummyIFormattable.ToString(), {string.Format(null, "{0:tt}", dateTime)}, {string.Format(null, "{0,5:tt}", dateTime)} + + notExpected: 0 + actual: 0 + + Assert.AreNotEqual(0, 0) + """); o.WasToStringCalled.Should().BeTrue(); } @@ -1411,51 +1500,68 @@ public void AreEqualStringDifferenceAtBeginning() { Action action = () => Assert.AreEqual("baaa", "aaaa"); action.Should().Throw() - .WithMessage(""" - Assert.AreEqual failed. String lengths are both 4 but differ at index 0. 'expected' expression: '"baaa"', 'actual' expression: '"aaaa"'. - Expected: "baaa" - But was: "aaaa" - -----------^ - """); + .Which.Message.Should().Be( + """ + Assertion failed. Expected strings to be equal. + String lengths are both 4 but differ at index 0. + + expected: "baaa" + actual: "aaaa" + + Assert.AreEqual("baaa", "aaaa") + """); } public void AreEqualStringDifferenceAtEnd() { Action action = () => Assert.AreEqual("aaaa", "aaab"); action.Should().Throw() - .WithMessage(""" - Assert.AreEqual failed. String lengths are both 4 but differ at index 3. 'expected' expression: '"aaaa"', 'actual' expression: '"aaab"'. - Expected: "aaaa" - But was: "aaab" - --------------^ - """); + .Which.Message.Should().Be( + """ + Assertion failed. Expected strings to be equal. + String lengths are both 4 but differ at index 3. + + expected: "aaaa" + actual: "aaab" + + Assert.AreEqual("aaaa", "aaab") + """); } public void AreEqualStringWithSpecialCharactersShouldEscape() { Action action = () => Assert.AreEqual("aa\ta", "aa a"); action.Should().Throw() - .WithMessage(""" - Assert.AreEqual failed. String lengths are both 4 but differ at index 2. 'expected' expression: '"aa\ta"', 'actual' expression: '"aa a"'. - Expected: "aa␉a" - But was: "aa a" - -------------^ - """); + .Which.Message.Should().Be( + """ + Assertion failed. Expected strings to be equal. + String lengths are both 4 but differ at index 2. + + expected: "aa\ta" + actual: "aa a" + + Assert.AreEqual("aa\ta", "aa a") + """); } - public void AreEqualLongStringsShouldTruncateAndShowContext() + // Long-string truncation is intentionally not yet implemented; documents the current full-string render. + public void AreEqualLongStringsShowsFullStrings() { string expected = new string('a', 100) + "b" + new string('c', 100); string actual = new string('a', 100) + "d" + new string('c', 100); Action action = () => Assert.AreEqual(expected, actual); action.Should().Throw() - .WithMessage(""" - Assert.AreEqual failed. String lengths are both 201 but differ at index 100. 'expected' expression: 'expected', 'actual' expression: 'actual'. - Expected: "...aaaaaaaaaaaaaaaaaabcccccccccccccccc..." - But was: "...aaaaaaaaaaaaaaaaaadcccccccccccccccc..." - --------------------------------^ - """); + .Which.Message.Should().Be( + """ + Assertion failed. Expected strings to be equal. + String lengths are both 201 but differ at index 100. + + expected: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + actual: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaadcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + + Assert.AreEqual(expected, actual) + """); } public void AreEqualStringWithCultureShouldUseEnhancedMessage() @@ -1474,48 +1580,65 @@ public void AreEqualStringWithDifferentLength() { Action action = () => Assert.AreEqual("aaaa", "aaa"); action.Should().Throw() - .WithMessage(""" - Assert.AreEqual failed. Expected string length 4 but was 3. 'expected' expression: '"aaaa"', 'actual' expression: '"aaa"'. - Expected: "aaaa" - But was: "aaa" - --------------^ - """); + .Which.Message.Should().Be( + """ + Assertion failed. Expected strings to be equal. + Expected string length 4 but was 3. + + expected: "aaaa" + actual: "aaa" + + Assert.AreEqual("aaaa", "aaa") + """); } public void AreEqualShorterExpectedString() { Action action = () => Assert.AreEqual("aaa", "aaab"); action.Should().Throw() - .WithMessage(""" - Assert.AreEqual failed. Expected string length 3 but was 4. 'expected' expression: '"aaa"', 'actual' expression: '"aaab"'. - Expected: "aaa" - But was: "aaab" - --------------^ - """); + .Which.Message.Should().Be( + """ + Assertion failed. Expected strings to be equal. + Expected string length 3 but was 4. + + expected: "aaa" + actual: "aaab" + + Assert.AreEqual("aaa", "aaab") + """); } public void AreEqualStringWithUserMessage() { Action action = () => Assert.AreEqual("aaaa", "aaab", "My custom message"); action.Should().Throw() - .WithMessage(""" - Assert.AreEqual failed. String lengths are both 4 but differ at index 3. 'expected' expression: '"aaaa"', 'actual' expression: '"aaab"'. My custom message - Expected: "aaaa" - But was: "aaab" - --------------^ - """); + .Which.Message.Should().Be( + """ + Assertion failed. Expected strings to be equal. + String lengths are both 4 but differ at index 3. + My custom message + + expected: "aaaa" + actual: "aaab" + + Assert.AreEqual("aaaa", "aaab") + """); } public void AreEqualStringWithEmojis() { Action action = () => Assert.AreEqual("🥰", "aaab"); action.Should().Throw() - .WithMessage(""" - Assert.AreEqual failed. Expected string length 2 but was 4. 'expected' expression: '"🥰"', 'actual' expression: '"aaab"'. - Expected: "🥰" - But was: "aaab" - -----------^ - """); + .Which.Message.Should().Be( + """ + Assertion failed. Expected strings to be equal. + Expected string length 2 but was 4. + + expected: "🥰" + actual: "aaab" + + Assert.AreEqual("🥰", "aaab") + """); } public void CreateStringPreviews_DiffPointsToCorrectPlaceInNonShortenedString() diff --git a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.ScopeTests.cs b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.ScopeTests.cs index fe99ada571..ab29d158f6 100644 --- a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.ScopeTests.cs +++ b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.ScopeTests.cs @@ -7,6 +7,18 @@ namespace Microsoft.VisualStudio.TestPlatform.TestFramework.UnitTests; public partial class AssertTests { + // Shared expected message for the structured failure produced by `Assert.AreEqual(1, 2)`. + // Used across the scope soft-failure tests below to reduce churn when the structured-message + // format evolves; update this single constant rather than every assertion site. + private const string AreEqual1And2StructuredMessage = """ + Assertion failed. Expected values to be equal. + + expected: 1 + actual: 2 + + Assert.AreEqual(1, 2) + """; + public void Scope_NoFailures_DoesNotThrow() { Action action = () => @@ -28,7 +40,7 @@ public void Scope_SingleFailure_ThrowsOnDispose() Action action = () => scope.Dispose(); action.Should().Throw() - .WithMessage("Assert.AreEqual failed. Expected:<1>. Actual:<2>. 'expected' expression: '1', 'actual' expression: '2'."); + .WithMessage(AreEqual1And2StructuredMessage); } public void Scope_MultipleFailures_CollectsAllErrors() @@ -48,7 +60,7 @@ public void Scope_MultipleFailures_CollectsAllErrors() .Which; innerException.InnerExceptions.Should().HaveCount(2); - innerException.InnerExceptions[0].Message.Should().Be("Assert.AreEqual failed. Expected:<1>. Actual:<2>. 'expected' expression: '1', 'actual' expression: '2'."); + innerException.InnerExceptions[0].Message.Should().Be(AreEqual1And2StructuredMessage); innerException.InnerExceptions[1].Message.Should().Be( """ Assertion failed. Expected condition to be true. @@ -102,7 +114,7 @@ public void Scope_DoubleDispose_DoesNotThrowTwice() Action firstDispose = () => scope.Dispose(); firstDispose.Should().Throw() - .WithMessage("Assert.AreEqual failed. Expected:<1>. Actual:<2>. 'expected' expression: '1', 'actual' expression: '2'."); + .WithMessage(AreEqual1And2StructuredMessage); // Second dispose should be a no-op Action secondDispose = () => scope.Dispose(); @@ -152,7 +164,7 @@ Assertion failed. Expected value to not be null. Assert.IsNotNull(value) """); - innerException.InnerExceptions[1].Message.Should().Be("Assert.AreEqual failed. Expected:<1>. Actual:<2>. 'expected' expression: '1', 'actual' expression: '2'."); + innerException.InnerExceptions[1].Message.Should().Be(AreEqual1And2StructuredMessage); } public void Scope_AssertIsInstanceOfType_IsSoftFailure() @@ -183,7 +195,7 @@ Assertion failed. Expected value to be of type Int32 (or derived). Assert.IsInstanceOfType(value) """); - innerException.InnerExceptions[1].Message.Should().Be("Assert.AreEqual failed. Expected:<1>. Actual:<2>. 'expected' expression: '1', 'actual' expression: '2'."); + innerException.InnerExceptions[1].Message.Should().Be(AreEqual1And2StructuredMessage); } public void Scope_AssertIsExactInstanceOfType_IsSoftFailure() @@ -214,7 +226,7 @@ Assertion failed. Expected value to be exactly of type Object. Assert.IsExactInstanceOfType(value) """); - innerException.InnerExceptions[1].Message.Should().Be("Assert.AreEqual failed. Expected:<1>. Actual:<2>. 'expected' expression: '1', 'actual' expression: '2'."); + innerException.InnerExceptions[1].Message.Should().Be(AreEqual1And2StructuredMessage); } public void Scope_AssertContainsSingle_IsSoftFailure() @@ -237,12 +249,14 @@ public void Scope_AssertContainsSingle_IsSoftFailure() innerException.InnerExceptions.Should().HaveCount(2); innerException.InnerExceptions[0].Message.Should().Be( - $"Assertion failed. Expected collection to contain exactly one element.{Environment.NewLine}" + - $"{Environment.NewLine}" + - $"expected count: 1{Environment.NewLine}" + - $"actual count: 3{Environment.NewLine}" + - $"{Environment.NewLine}" + - $"Assert.ContainsSingle(items)"); - innerException.InnerExceptions[1].Message.Should().Be("Assert.AreEqual failed. Expected:<1>. Actual:<2>. 'expected' expression: '1', 'actual' expression: '2'."); + """ + Assertion failed. Expected collection to contain exactly one element. + + expected count: 1 + actual count: 3 + + Assert.ContainsSingle(items) + """); + innerException.InnerExceptions[1].Message.Should().Be(AreEqual1And2StructuredMessage); } }