From 119afd57eb52829ce706d061256471f453a5efe8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Tue, 12 May 2026 18:06:00 +0200 Subject: [PATCH 1/5] Add structured assertion message infrastructure (RFC 012) Introduce the foundational types and helpers for structured multi-line assertion failure messages as described in RFC 012: - EvidenceLine: labeled line record struct for evidence blocks - EvidenceBlock: collection of labeled lines with automatic alignment - StructuredAssertionMessage: builder producing the new multi-line format (prefix + summary + user message + evidence block + call-site) - AssertionValueRenderer: renders values per RFC 012 rules (null, quoted strings with escape sequences, booleans, collections as JSON arrays) - AssertFailedException: add ExpectedText/ActualText public properties - Assert: add ReportAssertFailed/ThrowAssertFailed overloads accepting StructuredAssertionMessage No existing assertion methods are changed yet - this PR only introduces the infrastructure that subsequent PRs will use to migrate each assertion method to the new format. --- .../TestFramework/Assertions/Assert.cs | 58 +++++++ .../Assertions/AssertionValueRenderer.cs | 117 +++++++++++++ .../TestFramework/Assertions/EvidenceBlock.cs | 67 +++++++ .../TestFramework/Assertions/EvidenceLine.cs | 9 + .../Assertions/StructuredAssertionMessage.cs | 129 ++++++++++++++ .../Exceptions/AssertFailedException.cs | 14 ++ .../PublicAPI/PublicAPI.Unshipped.txt | 2 + .../Assertions/AssertFailedExceptionTests.cs | 39 +++++ .../Assertions/AssertionValueRendererTests.cs | 100 +++++++++++ .../Assertions/EvidenceBlockTests.cs | 77 +++++++++ .../StructuredAssertionMessageTests.cs | 163 ++++++++++++++++++ 11 files changed, 775 insertions(+) create mode 100644 src/TestFramework/TestFramework/Assertions/AssertionValueRenderer.cs create mode 100644 src/TestFramework/TestFramework/Assertions/EvidenceBlock.cs create mode 100644 src/TestFramework/TestFramework/Assertions/EvidenceLine.cs create mode 100644 src/TestFramework/TestFramework/Assertions/StructuredAssertionMessage.cs create mode 100644 test/UnitTests/TestFramework.UnitTests/Assertions/AssertFailedExceptionTests.cs create mode 100644 test/UnitTests/TestFramework.UnitTests/Assertions/AssertionValueRendererTests.cs create mode 100644 test/UnitTests/TestFramework.UnitTests/Assertions/EvidenceBlockTests.cs create mode 100644 test/UnitTests/TestFramework.UnitTests/Assertions/StructuredAssertionMessageTests.cs diff --git a/src/TestFramework/TestFramework/Assertions/Assert.cs b/src/TestFramework/TestFramework/Assertions/Assert.cs index 0703444ae6..7e565a24b4 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.cs @@ -107,6 +107,64 @@ static bool ShouldLaunchDebugger() private static AssertFailedException CreateAssertFailedException(string assertionName, string? message) => new(FormatAssertionFailed(assertionName, message)); + private static AssertFailedException CreateAssertFailedException(StructuredAssertionMessage structuredMessage) + { + AssertFailedException exception = new(structuredMessage.Format()) + { + ExpectedText = structuredMessage.ExpectedText, + ActualText = structuredMessage.ActualText, + }; + return exception; + } + + /// + /// Reports an assertion failure using a structured message. Within an , + /// the failure is collected and execution continues. Outside a scope, the failure is thrown immediately. + /// + /// + /// The structured assertion failure message. + /// +#pragma warning disable CS8763 // A method marked [DoesNotReturn] should not return + [DoesNotReturn] + [StackTraceHidden] + internal static void ReportAssertFailed(StructuredAssertionMessage structuredMessage) + { + LaunchDebuggerIfNeeded(); + AssertFailedException assertionFailedException = CreateAssertFailedException(structuredMessage); + if (AssertScope.Current is { } scope) + { + try + { + throw assertionFailedException; + } + catch (AssertFailedException ex) + { + assertionFailedException = ex; + } + + scope.AddError(assertionFailedException); + return; + } + + throw assertionFailedException; + } +#pragma warning restore CS8763 // A method marked [DoesNotReturn] should not return + + /// + /// Reports an assertion failure using a structured message and always throws, + /// even within an . + /// + /// + /// The structured assertion failure message. + /// + [DoesNotReturn] + [StackTraceHidden] + internal static void ThrowAssertFailed(StructuredAssertionMessage structuredMessage) + { + LaunchDebuggerIfNeeded(); + throw CreateAssertFailedException(structuredMessage); + } + private static string FormatAssertionFailed(string assertionName, string? message) { string failedMessage = string.Format(CultureInfo.CurrentCulture, FrameworkMessages.AssertionFailed, assertionName); diff --git a/src/TestFramework/TestFramework/Assertions/AssertionValueRenderer.cs b/src/TestFramework/TestFramework/Assertions/AssertionValueRenderer.cs new file mode 100644 index 0000000000..ba610f4c22 --- /dev/null +++ b/src/TestFramework/TestFramework/Assertions/AssertionValueRenderer.cs @@ -0,0 +1,117 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#pragma warning disable IDE0046 // Convert to conditional expression + +namespace Microsoft.VisualStudio.TestTools.UnitTesting; + +/// +/// Renders values for display in structured assertion messages following the RFC 012 value rendering rules. +/// +internal static class AssertionValueRenderer +{ + /// + /// Renders a value as a string suitable for display in the evidence block. + /// + internal static string RenderValue(object? value) + { + if (value is null) + { + return "null"; + } + + return value switch + { + string s => RenderString(s), + bool b => b ? "true" : "false", + char c => RenderChar(c), + IEnumerable enumerable when value is not string => RenderCollection(enumerable), + _ => value.ToString() ?? value.GetType().FullName ?? value.GetType().Name, + }; + } + + /// + /// Renders a string value with double quotes and escape sequences for control characters. + /// + private static string RenderString(string value) + { + StringBuilder sb = new(value.Length + 2); + sb.Append('"'); + foreach (char c in value) + { + switch (c) + { + case '"': + sb.Append("\\\""); + break; + case '\\': + sb.Append("\\\\"); + break; + case '\n': + sb.Append("\\n"); + break; + case '\r': + sb.Append("\\r"); + break; + case '\t': + sb.Append("\\t"); + break; + case '\0': + sb.Append("\\0"); + break; + default: + if (char.IsControl(c)) + { + sb.Append("\\u"); + sb.Append(((int)c).ToString("X4", CultureInfo.InvariantCulture)); + } + else + { + sb.Append(c); + } + + break; + } + } + + sb.Append('"'); + return sb.ToString(); + } + + /// + /// Renders a char value with single quotes and escape sequences. + /// + private static string RenderChar(char value) => + value switch + { + '\n' => "'\\n'", + '\r' => "'\\r'", + '\t' => "'\\t'", + '\0' => "'\\0'", + _ when char.IsControl(value) => $"'\\u{(int)value:X4}'", + _ => $"'{value}'", + }; + + /// + /// Renders a collection in JSON-style array notation. + /// + private static string RenderCollection(IEnumerable enumerable) + { + StringBuilder sb = new(); + sb.Append('['); + bool first = true; + foreach (object? item in enumerable) + { + if (!first) + { + sb.Append(", "); + } + + sb.Append(RenderValue(item)); + first = false; + } + + sb.Append(']'); + return sb.ToString(); + } +} diff --git a/src/TestFramework/TestFramework/Assertions/EvidenceBlock.cs b/src/TestFramework/TestFramework/Assertions/EvidenceBlock.cs new file mode 100644 index 0000000000..4fafbb4f8f --- /dev/null +++ b/src/TestFramework/TestFramework/Assertions/EvidenceBlock.cs @@ -0,0 +1,67 @@ +// 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; + +/// +/// Represents the evidence block of a structured assertion message, containing labeled value lines +/// such as expected/actual values and assertion-specific details. +/// +internal readonly struct EvidenceBlock +{ + private readonly List _lines; + + private EvidenceBlock(List lines) + { + _lines = lines; + } + + internal static EvidenceBlock Create() => new([]); + + internal IReadOnlyList Lines => _lines; + + internal EvidenceBlock AddLine(string label, string value) + { + _lines.Add(new EvidenceLine(label, value)); + return this; + } + + /// + /// Formats the evidence block as aligned label: value lines. + /// Labels are right-padded so all values start at the same column. + /// + internal string Format() + { + if (_lines.Count == 0) + { + return string.Empty; + } + + int maxLabelLength = 0; + foreach (EvidenceLine line in _lines) + { + if (line.Label.Length > maxLabelLength) + { + maxLabelLength = line.Label.Length; + } + } + + StringBuilder sb = new(); + for (int i = 0; i < _lines.Count; i++) + { + if (i > 0) + { + sb.Append(Environment.NewLine); + } + + EvidenceLine line = _lines[i]; + + // Pad label to align values, then append ": " and value + sb.Append(line.Label.PadRight(maxLabelLength)); + sb.Append(' '); + sb.Append(line.Value); + } + + return sb.ToString(); + } +} diff --git a/src/TestFramework/TestFramework/Assertions/EvidenceLine.cs b/src/TestFramework/TestFramework/Assertions/EvidenceLine.cs new file mode 100644 index 0000000000..bb09f5f150 --- /dev/null +++ b/src/TestFramework/TestFramework/Assertions/EvidenceLine.cs @@ -0,0 +1,9 @@ +// 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; + +/// +/// Represents a single labeled line in the evidence block of a structured assertion message. +/// +internal readonly record struct EvidenceLine(string Label, string Value); diff --git a/src/TestFramework/TestFramework/Assertions/StructuredAssertionMessage.cs b/src/TestFramework/TestFramework/Assertions/StructuredAssertionMessage.cs new file mode 100644 index 0000000000..cfb3f0fe0a --- /dev/null +++ b/src/TestFramework/TestFramework/Assertions/StructuredAssertionMessage.cs @@ -0,0 +1,129 @@ +// 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; + +/// +/// Builds a structured assertion failure message following the format: +/// +/// Assertion failed. <summary> +/// <user message> +/// +/// <evidence block> +/// +/// <call-site expression> +/// +/// +internal sealed class StructuredAssertionMessage +{ + private const string AssertionPrefix = "Assertion failed."; + + private readonly string _summary; + private readonly List _additionalSummaryLines = []; + private string? _userMessage; + private EvidenceBlock? _evidenceBlock; + private string? _callSiteExpression; + + internal StructuredAssertionMessage(string summary) + { + _summary = summary; + } + + internal string? ExpectedText { get; private set; } + + internal string? ActualText { get; private set; } + + internal StructuredAssertionMessage WithAdditionalSummaryLine(string line) + { + _additionalSummaryLines.Add(line); + return this; + } + + internal StructuredAssertionMessage WithUserMessage(string? userMessage) + { + if (!string.IsNullOrWhiteSpace(userMessage)) + { + _userMessage = userMessage; + } + + return this; + } + + internal StructuredAssertionMessage WithEvidence(EvidenceBlock evidenceBlock) + { + _evidenceBlock = evidenceBlock; + return this; + } + + internal StructuredAssertionMessage WithExpectedAndActual(string? expectedText, string? actualText) + { + ExpectedText = expectedText; + ActualText = actualText; + return this; + } + + internal StructuredAssertionMessage WithCallSiteExpression(string? callSiteExpression) + { + if (!string.IsNullOrWhiteSpace(callSiteExpression)) + { + _callSiteExpression = callSiteExpression; + } + + return this; + } + + /// + /// Formats the structured message as a multi-line string following the RFC 012 layout. + /// + internal string Format() + { + StringBuilder sb = new(); + + // Line 1: Assertion prefix + summary + sb.Append(AssertionPrefix); + if (!string.IsNullOrEmpty(_summary)) + { + sb.Append(' '); + sb.Append(_summary); + } + + // Additional summary lines + foreach (string additionalLine in _additionalSummaryLines) + { + sb.Append(Environment.NewLine); + sb.Append(additionalLine); + } + + // User message (on its own line, no label) + if (_userMessage is not null) + { + sb.Append(Environment.NewLine); + sb.Append(_userMessage); + } + + // Evidence block (separated by blank line) + if (_evidenceBlock is { } evidence) + { + string formattedEvidence = evidence.Format(); + if (!string.IsNullOrEmpty(formattedEvidence)) + { + sb.Append(Environment.NewLine); + sb.Append(Environment.NewLine); + sb.Append(formattedEvidence); + } + } + + // Call-site expression (separated by blank line) + if (_callSiteExpression is not null) + { + sb.Append(Environment.NewLine); + sb.Append(Environment.NewLine); + sb.Append(_callSiteExpression); + } + + return sb.ToString(); + } + + /// + public override string ToString() => Format(); +} diff --git a/src/TestFramework/TestFramework/Exceptions/AssertFailedException.cs b/src/TestFramework/TestFramework/Exceptions/AssertFailedException.cs index 2bbd14c132..42a8b22397 100644 --- a/src/TestFramework/TestFramework/Exceptions/AssertFailedException.cs +++ b/src/TestFramework/TestFramework/Exceptions/AssertFailedException.cs @@ -39,6 +39,20 @@ public AssertFailedException() { } + /// + /// Gets the pre-formatted text representation of the expected value, as displayed + /// in the expected: line of the structured assertion message. Returns + /// when the assertion has no natural expected value (e.g. ). + /// + public string? ExpectedText { get; internal set; } + + /// + /// Gets the pre-formatted text representation of the actual value, as displayed + /// in the actual: line of the structured assertion message. Returns + /// when the assertion has no natural actual value. + /// + public string? ActualText { get; internal set; } + /// /// Initializes a new instance of the class. /// diff --git a/src/TestFramework/TestFramework/PublicAPI/PublicAPI.Unshipped.txt b/src/TestFramework/TestFramework/PublicAPI/PublicAPI.Unshipped.txt index ee81f202d3..cc143fac2e 100644 --- a/src/TestFramework/TestFramework/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/TestFramework/TestFramework/PublicAPI/PublicAPI.Unshipped.txt @@ -1,2 +1,4 @@ #nullable enable [MSTESTEXP]static Microsoft.VisualStudio.TestTools.UnitTesting.Assert.Scope() -> System.IDisposable! +Microsoft.VisualStudio.TestTools.UnitTesting.AssertFailedException.ActualText.get -> string? +Microsoft.VisualStudio.TestTools.UnitTesting.AssertFailedException.ExpectedText.get -> string? diff --git a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertFailedExceptionTests.cs b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertFailedExceptionTests.cs new file mode 100644 index 0000000000..62c82fd0a4 --- /dev/null +++ b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertFailedExceptionTests.cs @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using AwesomeAssertions; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using TestFramework.ForTestingMSTest; + +namespace Microsoft.VisualStudio.TestPlatform.TestFramework.UnitTests; + +public class AssertFailedExceptionTests : TestContainer +{ + public void ExpectedText_DefaultsToNull() + { + var exception = new AssertFailedException("test message"); + + exception.ExpectedText.Should().BeNull(); + } + + public void ActualText_DefaultsToNull() + { + var exception = new AssertFailedException("test message"); + + exception.ActualText.Should().BeNull(); + } + + public void ExpectedAndActualText_CanBeSet() + { + var exception = new AssertFailedException("test message") + { + ExpectedText = "42", + ActualText = "37", + }; + + exception.ExpectedText.Should().Be("42"); + exception.ActualText.Should().Be("37"); + } +} diff --git a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertionValueRendererTests.cs b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertionValueRendererTests.cs new file mode 100644 index 0000000000..dda2606d14 --- /dev/null +++ b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertionValueRendererTests.cs @@ -0,0 +1,100 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using AwesomeAssertions; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using TestFramework.ForTestingMSTest; + +namespace Microsoft.VisualStudio.TestPlatform.TestFramework.UnitTests; + +public class AssertionValueRendererTests : TestContainer +{ + public void RenderValue_Null_ReturnsNull() => + AssertionValueRenderer.RenderValue(null).Should().Be("null"); + + public void RenderValue_EmptyString_ReturnsQuotedEmpty() => + AssertionValueRenderer.RenderValue(string.Empty).Should().Be("\"\""); + + public void RenderValue_SimpleString_ReturnsQuotedString() => + AssertionValueRenderer.RenderValue("hello world").Should().Be("\"hello world\""); + + public void RenderValue_StringWithEmbeddedQuotes_EscapesQuotes() => + AssertionValueRenderer.RenderValue("she said \"hello\"").Should().Be("\"she said \\\"hello\\\"\""); + + public void RenderValue_StringWithNewline_EscapesNewline() => + AssertionValueRenderer.RenderValue("line1\nline2").Should().Be("\"line1\\nline2\""); + + public void RenderValue_StringWithCarriageReturn_EscapesCR() => + AssertionValueRenderer.RenderValue("line1\rline2").Should().Be("\"line1\\rline2\""); + + public void RenderValue_StringWithTab_EscapesTab() => + AssertionValueRenderer.RenderValue("col1\tcol2").Should().Be("\"col1\\tcol2\""); + + public void RenderValue_StringWithNullChar_EscapesNull() => + AssertionValueRenderer.RenderValue("abc\0def").Should().Be("\"abc\\0def\""); + + public void RenderValue_StringWithBackslash_EscapesBackslash() => + AssertionValueRenderer.RenderValue("path\\to\\file").Should().Be("\"path\\\\to\\\\file\""); + + public void RenderValue_WhitespaceOnlyString_ReturnsQuotedWhitespace() => + AssertionValueRenderer.RenderValue(" ").Should().Be("\" \""); + + public void RenderValue_Integer_ReturnsUnquoted() => + AssertionValueRenderer.RenderValue(42).Should().Be("42"); + + public void RenderValue_NegativeInteger_ReturnsUnquoted() => + AssertionValueRenderer.RenderValue(-7).Should().Be("-7"); + + public void RenderValue_Double_ReturnsUnquoted() => + AssertionValueRenderer.RenderValue(3.14).Should().Be("3.14"); + + public void RenderValue_BoolTrue_ReturnsLowercase() => + AssertionValueRenderer.RenderValue(true).Should().Be("true"); + + public void RenderValue_BoolFalse_ReturnsLowercase() => + AssertionValueRenderer.RenderValue(false).Should().Be("false"); + + public void RenderValue_ListOfInts_ReturnsJsonArray() + { + var list = new List { 1, 2, 3 }; + AssertionValueRenderer.RenderValue(list).Should().Be("[1, 2, 3]"); + } + + public void RenderValue_EmptyList_ReturnsEmptyBrackets() => + AssertionValueRenderer.RenderValue(new List()).Should().Be("[]"); + + public void RenderValue_ListOfStrings_ReturnsQuotedElements() + { + var list = new List { "apple", "cherry", "date" }; + AssertionValueRenderer.RenderValue(list).Should().Be("[\"apple\", \"cherry\", \"date\"]"); + } + + public void RenderValue_ListWithNull_RendersNullElement() + { + var list = new List { "apple", null, "date" }; + AssertionValueRenderer.RenderValue(list).Should().Be("[\"apple\", null, \"date\"]"); + } + + public void RenderValue_ObjectWithToString_ReturnsToString() => + AssertionValueRenderer.RenderValue(new ObjectWithCustomToString("my-object")).Should().Be("my-object"); + + public void RenderValue_Char_ReturnsSingleQuoted() => + AssertionValueRenderer.RenderValue('a').Should().Be("'a'"); + + public void RenderValue_CharNewline_ReturnsEscaped() => + AssertionValueRenderer.RenderValue('\n').Should().Be("'\\n'"); + + private sealed class ObjectWithCustomToString + { + private readonly string _value; + + public ObjectWithCustomToString(string value) + { + _value = value; + } + + public override string ToString() => _value; + } +} diff --git a/test/UnitTests/TestFramework.UnitTests/Assertions/EvidenceBlockTests.cs b/test/UnitTests/TestFramework.UnitTests/Assertions/EvidenceBlockTests.cs new file mode 100644 index 0000000000..9bc0a523a1 --- /dev/null +++ b/test/UnitTests/TestFramework.UnitTests/Assertions/EvidenceBlockTests.cs @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using AwesomeAssertions; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using TestFramework.ForTestingMSTest; + +namespace Microsoft.VisualStudio.TestPlatform.TestFramework.UnitTests; + +public class EvidenceBlockTests : TestContainer +{ + public void Format_EmptyBlock_ReturnsEmptyString() + { + var block = EvidenceBlock.Create(); + + string result = block.Format(); + + result.Should().BeEmpty(); + } + + public void Format_SingleLine_FormatsLabelAndValue() + { + EvidenceBlock block = EvidenceBlock.Create() + .AddLine("expected:", "42"); + + string result = block.Format(); + + result.Should().Be("expected: 42"); + } + + public void Format_TwoLines_AlignsLabels() + { + EvidenceBlock block = EvidenceBlock.Create() + .AddLine("expected:", "42") + .AddLine("actual:", "37"); + + string result = block.Format(); + + // "expected:" is 9 chars, "actual:" is 7 chars + // "actual:" should be padded to 9 chars for alignment + string expected = "expected: 42" + Environment.NewLine + "actual: 37"; + result.Should().Be(expected); + } + + public void Format_MultipleLines_AlignsToLongestLabel() + { + EvidenceBlock block = EvidenceBlock.Create() + .AddLine("expected:", "42") + .AddLine("actual:", "37") + .AddLine("ignore case:", "true") + .AddLine("culture:", "tr-TR"); + + string result = block.Format(); + + string[] lines = result.Split([Environment.NewLine], StringSplitOptions.None); + lines.Should().HaveCount(4); + lines[0].Should().Be("expected: 42"); + lines[1].Should().Be("actual: 37"); + lines[2].Should().Be("ignore case: true"); + lines[3].Should().Be("culture: tr-TR"); + } + + public void Lines_ReturnsAddedLines() + { + EvidenceBlock block = EvidenceBlock.Create() + .AddLine("expected:", "42") + .AddLine("actual:", "37"); + + block.Lines.Should().HaveCount(2); + block.Lines[0].Label.Should().Be("expected:"); + block.Lines[0].Value.Should().Be("42"); + block.Lines[1].Label.Should().Be("actual:"); + block.Lines[1].Value.Should().Be("37"); + } +} diff --git a/test/UnitTests/TestFramework.UnitTests/Assertions/StructuredAssertionMessageTests.cs b/test/UnitTests/TestFramework.UnitTests/Assertions/StructuredAssertionMessageTests.cs new file mode 100644 index 0000000000..b30d620ca4 --- /dev/null +++ b/test/UnitTests/TestFramework.UnitTests/Assertions/StructuredAssertionMessageTests.cs @@ -0,0 +1,163 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using AwesomeAssertions; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using TestFramework.ForTestingMSTest; + +namespace Microsoft.VisualStudio.TestPlatform.TestFramework.UnitTests; + +public class StructuredAssertionMessageTests : TestContainer +{ + public void Format_SummaryOnly_ReturnsAssertionPrefix() + { + StructuredAssertionMessage message = new("Expected values to be equal."); + + string result = message.Format(); + + result.Should().Be("Assertion failed. Expected values to be equal."); + } + + public void Format_EmptySummary_ReturnsJustPrefix() + { + StructuredAssertionMessage message = new(string.Empty); + + string result = message.Format(); + + result.Should().Be("Assertion failed."); + } + + public void Format_WithUserMessage_ShowsMessageAfterSummary() + { + StructuredAssertionMessage message = new("Expected values to be equal."); + message.WithUserMessage("Discount should be applied after tax"); + + string result = message.Format(); + + string expected = "Assertion failed. Expected values to be equal." + Environment.NewLine + + "Discount should be applied after tax"; + result.Should().Be(expected); + } + + public void Format_WithEvidenceBlock_SeparatedByBlankLine() + { + EvidenceBlock evidence = EvidenceBlock.Create() + .AddLine("expected:", "42") + .AddLine("actual:", "37"); + + StructuredAssertionMessage message = new("Expected values to be equal."); + message.WithEvidence(evidence); + + string result = message.Format(); + + string expected = "Assertion failed. Expected values to be equal." + Environment.NewLine + + Environment.NewLine + + "expected: 42" + Environment.NewLine + + "actual: 37"; + result.Should().Be(expected); + } + + public void Format_WithUserMessageAndEvidence_CorrectLayout() + { + EvidenceBlock evidence = EvidenceBlock.Create() + .AddLine("expected:", "42") + .AddLine("actual:", "37"); + + StructuredAssertionMessage message = new("Expected values to be equal."); + message.WithUserMessage("Discount should be applied after tax"); + message.WithEvidence(evidence); + + string result = message.Format(); + + string expected = "Assertion failed. Expected values to be equal." + Environment.NewLine + + "Discount should be applied after tax" + Environment.NewLine + + Environment.NewLine + + "expected: 42" + Environment.NewLine + + "actual: 37"; + result.Should().Be(expected); + } + + public void Format_WithCallSiteExpression_SeparatedByBlankLine() + { + EvidenceBlock evidence = EvidenceBlock.Create() + .AddLine("expected:", "42") + .AddLine("actual:", "37"); + + StructuredAssertionMessage message = new("Expected values to be equal."); + message.WithEvidence(evidence); + message.WithCallSiteExpression("Assert.AreEqual(expectedCount, actualCount)"); + + string result = message.Format(); + + string expected = "Assertion failed. Expected values to be equal." + Environment.NewLine + + Environment.NewLine + + "expected: 42" + Environment.NewLine + + "actual: 37" + Environment.NewLine + + Environment.NewLine + + "Assert.AreEqual(expectedCount, actualCount)"; + result.Should().Be(expected); + } + + public void Format_FullMessage_CorrectLayout() + { + EvidenceBlock evidence = EvidenceBlock.Create() + .AddLine("expected:", "42") + .AddLine("actual:", "37"); + + StructuredAssertionMessage message = new("Expected values to be equal."); + message.WithAdditionalSummaryLine("Values differ at position 3."); + message.WithUserMessage("Check the discount logic"); + message.WithEvidence(evidence); + message.WithCallSiteExpression("Assert.AreEqual(expected, actual)"); + + string result = message.Format(); + + string expected = "Assertion failed. Expected values to be equal." + Environment.NewLine + + "Values differ at position 3." + Environment.NewLine + + "Check the discount logic" + Environment.NewLine + + Environment.NewLine + + "expected: 42" + Environment.NewLine + + "actual: 37" + Environment.NewLine + + Environment.NewLine + + "Assert.AreEqual(expected, actual)"; + result.Should().Be(expected); + } + + public void Format_NullUserMessage_OmitsUserMessageLine() + { + StructuredAssertionMessage message = new("Expected values to be equal."); + message.WithUserMessage(null); + + string result = message.Format(); + + result.Should().Be("Assertion failed. Expected values to be equal."); + } + + public void Format_WhitespaceUserMessage_OmitsUserMessageLine() + { + StructuredAssertionMessage message = new("Expected values to be equal."); + message.WithUserMessage(" "); + + string result = message.Format(); + + result.Should().Be("Assertion failed. Expected values to be equal."); + } + + public void WithExpectedAndActual_SetsProperties() + { + StructuredAssertionMessage message = new("Expected values to be equal."); + message.WithExpectedAndActual("42", "37"); + + message.ExpectedText.Should().Be("42"); + message.ActualText.Should().Be("37"); + } + + public void ToString_ReturnsSameAsFormat() + { + StructuredAssertionMessage message = new("Expected values to be equal."); + + message.ToString().Should().Be(message.Format()); + } +} From 00602201c444b0bd112de8f1d18689387b92b8eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Tue, 12 May 2026 18:51:22 +0200 Subject: [PATCH 2/5] Structured assertion messages for reference and type assertions Apply RFC 012 structured assertion message format to: - AreSame / AreNotSame (reference equality) - IsInstanceOfType / IsNotInstanceOfType (type checking) - IsExactInstanceOfType / IsNotExactInstanceOfType (exact type checking) Each method now produces structured output with summary, evidence block, user message, and call-site expression per the RFC specification. Update all corresponding test expectations to match the new format. --- .../Assertions/Assert.AreSame.cs | 75 ++++++++++--------- .../Assert.IsExactInstanceOfType.cs | 73 ++++++++++-------- .../Assertions/Assert.IsInstanceOfType.cs | 73 ++++++++++-------- .../TestFramework/Assertions/Assert.cs | 34 +++++++++ .../Assertions/AssertTests.AreSame.cs | 54 ++++++++----- .../AssertTests.IsExactInstanceOfTypeTests.cs | 34 ++++----- .../AssertTests.IsInstanceOfTypeTests.cs | 22 +++--- .../Assertions/AssertTests.ScopeTests.cs | 4 +- 8 files changed, 223 insertions(+), 146 deletions(-) diff --git a/src/TestFramework/TestFramework/Assertions/Assert.AreSame.cs b/src/TestFramework/TestFramework/Assertions/Assert.AreSame.cs index 9b92297c9d..9de25e7d0c 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.AreSame.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.AreSame.cs @@ -38,8 +38,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) + " "); - ReportAssertAreSameFailed(_expected, _actual, _builder.ToString()); + ReportAssertAreSameFailed(_expected, _actual, _builder.ToString(), expectedExpression, actualExpression); } } @@ -98,8 +97,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) + " "); - ReportAssertAreNotSameFailed(_notExpected, _actual, _builder.ToString()); + ReportAssertAreNotSameFailed(_notExpected, _actual, _builder.ToString(), notExpectedExpression, actualExpression); } } @@ -181,34 +179,37 @@ public static void AreSame(T? expected, T? actual, string? message = "", [Cal return; } - string userMessage = BuildUserMessageForExpectedExpressionAndActualExpression(message, expectedExpression, actualExpression); - ReportAssertAreSameFailed(expected, actual, userMessage); + ReportAssertAreSameFailed(expected, actual, message, expectedExpression, actualExpression); } private static bool IsAreSameFailing(T? expected, T? actual) => !object.ReferenceEquals(expected, actual); [DoesNotReturn] - private static void ReportAssertAreSameFailed(T? expected, T? actual, string userMessage) + private static void ReportAssertAreSameFailed(T? expected, T? actual, string? userMessage, string expectedExpression, string actualExpression) { - string finalMessage = expected is null - ? string.Format( - CultureInfo.CurrentCulture, - FrameworkMessages.AreSameExpectedIsNull, - userMessage) - : actual is null - ? string.Format( - CultureInfo.CurrentCulture, - FrameworkMessages.AreSameActualIsNull, - userMessage) - : expected is ValueType && actual is ValueType - ? string.Format( - CultureInfo.CurrentCulture, - FrameworkMessages.AreSameGivenValues, - userMessage) - : userMessage; - - ReportAssertFailed("Assert.AreSame", finalMessage); + StructuredAssertionMessage msg = new("Expected both values to refer to the same object."); + + if (expected is ValueType && actual is ValueType) + { + msg.WithAdditionalSummaryLine("Do not pass value types to AreSame \u2014 value types are boxed on each call, so references will never be the same."); + } + + msg.WithUserMessage(userMessage); + + if (expected is not ValueType || actual is not ValueType) + { + string expectedText = expected is null ? "null" : $"{expected.GetType()} (hash: 0x{RuntimeHelpers.GetHashCode(expected):X})"; + string actualText = actual is null ? "null" : $"{actual.GetType()} (hash: 0x{RuntimeHelpers.GetHashCode(actual):X})"; + EvidenceBlock evidence = EvidenceBlock.Create() + .AddLine("expected:", expectedText) + .AddLine("actual:", actualText); + msg.WithEvidence(evidence).WithExpectedAndActual(expectedText, actualText); + } + + msg.WithCallSiteExpression(FormatCallSiteExpression("Assert.AreSame", expectedExpression, actualExpression)); + + ReportAssertFailed(msg); } /// @@ -252,7 +253,7 @@ public static void AreNotSame(T? notExpected, T? actual, string? message = "" { if (IsAreNotSameFailing(notExpected, actual)) { - ReportAssertAreNotSameFailed(notExpected, actual, BuildUserMessageForNotExpectedExpressionAndActualExpression(message, notExpectedExpression, actualExpression)); + ReportAssertAreNotSameFailed(notExpected, actual, message, notExpectedExpression, actualExpression); } } @@ -260,15 +261,19 @@ private static bool IsAreNotSameFailing(T? notExpected, T? actual) => object.ReferenceEquals(notExpected, actual); [DoesNotReturn] - private static void ReportAssertAreNotSameFailed(T? notExpected, T? actual, string userMessage) + private static void ReportAssertAreNotSameFailed(T? notExpected, T? actual, string? userMessage, string notExpectedExpression, string actualExpression) { - string finalMessage = notExpected is null && actual is null - ? string.Format( - CultureInfo.CurrentCulture, - FrameworkMessages.AreNotSameBothNull, - userMessage) - : userMessage; - - ReportAssertFailed("Assert.AreNotSame", finalMessage); + StructuredAssertionMessage msg = new("Expected values to refer to different objects."); + + msg.WithAdditionalSummaryLine( + notExpected is null && actual is null + ? "Both values are null." + : "Both variables refer to the same object."); + + msg.WithUserMessage(userMessage); + + msg.WithCallSiteExpression(FormatCallSiteExpression("Assert.AreNotSame", notExpectedExpression, actualExpression)); + + ReportAssertFailed(msg); } } diff --git a/src/TestFramework/TestFramework/Assertions/Assert.IsExactInstanceOfType.cs b/src/TestFramework/TestFramework/Assertions/Assert.IsExactInstanceOfType.cs index 3404a7ac57..59f78089a4 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.IsExactInstanceOfType.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.IsExactInstanceOfType.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. using System.ComponentModel; @@ -38,8 +38,7 @@ internal void ComputeAssertion(string valueExpression) { if (_builder is not null) { - _builder.Insert(0, string.Format(CultureInfo.CurrentCulture, FrameworkMessages.CallerArgumentExpressionSingleParameterMessage, "value", valueExpression) + " "); - ReportAssertIsExactInstanceOfTypeFailed(_value, _expectedType, _builder.ToString()); + ReportAssertIsExactInstanceOfTypeFailed(_value, _expectedType, _builder.ToString(), valueExpression); } } @@ -98,8 +97,7 @@ internal void ComputeAssertion(string valueExpression) { if (_builder is not null) { - _builder.Insert(0, string.Format(CultureInfo.CurrentCulture, FrameworkMessages.CallerArgumentExpressionSingleParameterMessage, "value", valueExpression) + " "); - ReportAssertIsExactInstanceOfTypeFailed(_value, typeof(TArg), _builder.ToString()); + ReportAssertIsExactInstanceOfTypeFailed(_value, typeof(TArg), _builder.ToString(), valueExpression); } } @@ -160,8 +158,7 @@ internal void ComputeAssertion(string valueExpression) { if (_builder is not null) { - _builder.Insert(0, string.Format(CultureInfo.CurrentCulture, FrameworkMessages.CallerArgumentExpressionSingleParameterMessage, "value", valueExpression) + " "); - ReportAssertIsNotExactInstanceOfTypeFailed(_value, _wrongType, _builder.ToString()); + ReportAssertIsNotExactInstanceOfTypeFailed(_value, _wrongType, _builder.ToString(), valueExpression); } } @@ -220,8 +217,7 @@ internal void ComputeAssertion(string valueExpression) { if (_builder is not null) { - _builder.Insert(0, string.Format(CultureInfo.CurrentCulture, FrameworkMessages.CallerArgumentExpressionSingleParameterMessage, "value", valueExpression) + " "); - ReportAssertIsNotExactInstanceOfTypeFailed(_value, typeof(TArg), _builder.ToString()); + ReportAssertIsNotExactInstanceOfTypeFailed(_value, typeof(TArg), _builder.ToString(), valueExpression); } } @@ -291,7 +287,7 @@ public static void IsExactInstanceOfType([NotNull] object? value, [NotNull] Type { if (IsExactInstanceOfTypeFailing(value, expectedType)) { - ReportAssertIsExactInstanceOfTypeFailed(value, expectedType, BuildUserMessageForValueExpression(message, valueExpression)); + ReportAssertIsExactInstanceOfTypeFailed(value, expectedType, message, valueExpression); } } @@ -329,18 +325,31 @@ private static bool IsExactInstanceOfTypeFailing([NotNullWhen(false)] object? va => expectedType is null || value is null || value.GetType() != expectedType; [DoesNotReturn] - private static void ReportAssertIsExactInstanceOfTypeFailed(object? value, Type? expectedType, string userMessage) + private static void ReportAssertIsExactInstanceOfTypeFailed(object? value, Type? expectedType, string? userMessage, string valueExpression) { - string finalMessage = expectedType is not null - ? string.Format( - CultureInfo.CurrentCulture, - FrameworkMessages.IsExactInstanceOfFailMsg, - userMessage, - expectedType.ToString(), - value?.GetType().ToString() ?? "null") - : userMessage; - - ReportAssertFailed("Assert.IsExactInstanceOfType", finalMessage); + string typeName = expectedType?.Name ?? "null"; + StructuredAssertionMessage msg = new($"Expected value to be exactly of type {typeName}."); + msg.WithUserMessage(userMessage); + + if (expectedType is not null) + { + EvidenceBlock evidence = EvidenceBlock.Create() + .AddLine("expected type:", expectedType.ToString()); + if (value is null) + { + evidence.AddLine("actual:", "null"); + } + else + { + evidence.AddLine("actual type:", value.GetType().ToString()); + evidence.AddLine("actual value:", AssertionValueRenderer.RenderValue(value)); + } + + msg.WithEvidence(evidence); + } + + msg.WithCallSiteExpression(FormatCallSiteExpression("Assert.IsExactInstanceOfType", valueExpression)); + ReportAssertFailed(msg); } /// @@ -371,7 +380,7 @@ public static void IsNotExactInstanceOfType(object? value, [NotNull] Type? wrong { if (IsNotExactInstanceOfTypeFailing(value, wrongType)) { - ReportAssertIsNotExactInstanceOfTypeFailed(value, wrongType, BuildUserMessageForValueExpression(message, valueExpression)); + ReportAssertIsNotExactInstanceOfTypeFailed(value, wrongType, message, valueExpression); } } @@ -403,19 +412,21 @@ private static bool IsNotExactInstanceOfTypeFailing(object? value, [NotNullWhen( (value is not null && value.GetType() == wrongType); [DoesNotReturn] - private static void ReportAssertIsNotExactInstanceOfTypeFailed(object? value, Type? wrongType, string userMessage) + private static void ReportAssertIsNotExactInstanceOfTypeFailed(object? value, Type? wrongType, string? userMessage, string valueExpression) { - string finalMessage = userMessage; + string typeName = wrongType?.Name ?? "null"; + StructuredAssertionMessage msg = new($"Expected value to not be exactly of type {typeName}."); + msg.WithUserMessage(userMessage); + if (wrongType is not null) { - finalMessage = string.Format( - CultureInfo.CurrentCulture, - FrameworkMessages.IsNotExactInstanceOfFailMsg, - userMessage, - wrongType.ToString(), - value!.GetType().ToString()); + EvidenceBlock evidence = EvidenceBlock.Create() + .AddLine("not expected type:", wrongType.ToString()) + .AddLine("actual value:", AssertionValueRenderer.RenderValue(value)); + msg.WithEvidence(evidence); } - ReportAssertFailed("Assert.IsNotExactInstanceOfType", finalMessage); + msg.WithCallSiteExpression(FormatCallSiteExpression("Assert.IsNotExactInstanceOfType", valueExpression)); + ReportAssertFailed(msg); } } diff --git a/src/TestFramework/TestFramework/Assertions/Assert.IsInstanceOfType.cs b/src/TestFramework/TestFramework/Assertions/Assert.IsInstanceOfType.cs index 27f06595b0..e11b00fadd 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.IsInstanceOfType.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.IsInstanceOfType.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. using System.ComponentModel; @@ -38,8 +38,7 @@ internal void ComputeAssertion(string valueExpression) { if (_builder is not null) { - _builder.Insert(0, string.Format(CultureInfo.CurrentCulture, FrameworkMessages.CallerArgumentExpressionSingleParameterMessage, "value", valueExpression) + " "); - ReportAssertIsInstanceOfTypeFailed(_value, _expectedType, _builder.ToString()); + ReportAssertIsInstanceOfTypeFailed(_value, _expectedType, _builder.ToString(), valueExpression); } } @@ -98,8 +97,7 @@ internal void ComputeAssertion(string valueExpression) { if (_builder is not null) { - _builder.Insert(0, string.Format(CultureInfo.CurrentCulture, FrameworkMessages.CallerArgumentExpressionSingleParameterMessage, "value", valueExpression) + " "); - ReportAssertIsInstanceOfTypeFailed(_value, typeof(TArg), _builder.ToString()); + ReportAssertIsInstanceOfTypeFailed(_value, typeof(TArg), _builder.ToString(), valueExpression); } } @@ -160,8 +158,7 @@ internal void ComputeAssertion(string valueExpression) { if (_builder is not null) { - _builder.Insert(0, string.Format(CultureInfo.CurrentCulture, FrameworkMessages.CallerArgumentExpressionSingleParameterMessage, "value", valueExpression) + " "); - ReportAssertIsNotInstanceOfTypeFailed(_value, _wrongType, _builder.ToString()); + ReportAssertIsNotInstanceOfTypeFailed(_value, _wrongType, _builder.ToString(), valueExpression); } } @@ -220,8 +217,7 @@ internal void ComputeAssertion(string valueExpression) { if (_builder is not null) { - _builder.Insert(0, string.Format(CultureInfo.CurrentCulture, FrameworkMessages.CallerArgumentExpressionSingleParameterMessage, "value", valueExpression) + " "); - ReportAssertIsNotInstanceOfTypeFailed(_value, typeof(TArg), _builder.ToString()); + ReportAssertIsNotInstanceOfTypeFailed(_value, typeof(TArg), _builder.ToString(), valueExpression); } } @@ -292,7 +288,7 @@ public static void IsInstanceOfType([NotNull] object? value, [NotNull] Type? exp { if (IsInstanceOfTypeFailing(value, expectedType)) { - ReportAssertIsInstanceOfTypeFailed(value, expectedType, BuildUserMessageForValueExpression(message, valueExpression)); + ReportAssertIsInstanceOfTypeFailed(value, expectedType, message, valueExpression); } } @@ -331,18 +327,31 @@ private static bool IsInstanceOfTypeFailing([NotNullWhen(false)] object? value, => expectedType == null || value == null || !expectedType.IsInstanceOfType(value); [DoesNotReturn] - private static void ReportAssertIsInstanceOfTypeFailed(object? value, Type? expectedType, string userMessage) + private static void ReportAssertIsInstanceOfTypeFailed(object? value, Type? expectedType, string? userMessage, string valueExpression) { - string finalMessage = expectedType is not null - ? string.Format( - CultureInfo.CurrentCulture, - FrameworkMessages.IsInstanceOfFailMsg, - userMessage, - expectedType.ToString(), - value?.GetType().ToString() ?? "null") - : userMessage; - - ReportAssertFailed("Assert.IsInstanceOfType", finalMessage); + string typeName = expectedType?.Name ?? "null"; + StructuredAssertionMessage msg = new($"Expected value to be of type {typeName} (or derived)."); + msg.WithUserMessage(userMessage); + + if (expectedType is not null) + { + EvidenceBlock evidence = EvidenceBlock.Create() + .AddLine("expected type:", $"{expectedType} (or derived)"); + if (value is null) + { + evidence.AddLine("actual:", "null"); + } + else + { + evidence.AddLine("actual type:", value.GetType().ToString()); + evidence.AddLine("actual value:", AssertionValueRenderer.RenderValue(value)); + } + + msg.WithEvidence(evidence); + } + + msg.WithCallSiteExpression(FormatCallSiteExpression("Assert.IsInstanceOfType", valueExpression)); + ReportAssertFailed(msg); } /// @@ -374,7 +383,7 @@ public static void IsNotInstanceOfType(object? value, [NotNull] Type? wrongType, { if (IsNotInstanceOfTypeFailing(value, wrongType)) { - ReportAssertIsNotInstanceOfTypeFailed(value, wrongType, BuildUserMessageForValueExpression(message, valueExpression)); + ReportAssertIsNotInstanceOfTypeFailed(value, wrongType, message, valueExpression); } } @@ -407,19 +416,21 @@ private static bool IsNotInstanceOfTypeFailing(object? value, [NotNullWhen(false (value is not null && wrongType.IsInstanceOfType(value)); [DoesNotReturn] - private static void ReportAssertIsNotInstanceOfTypeFailed(object? value, Type? wrongType, string userMessage) + private static void ReportAssertIsNotInstanceOfTypeFailed(object? value, Type? wrongType, string? userMessage, string valueExpression) { - string finalMessage = userMessage; + string typeName = wrongType?.Name ?? "null"; + StructuredAssertionMessage msg = new($"Expected value to not be of type {typeName} (or derived)."); + msg.WithUserMessage(userMessage); + if (wrongType is not null) { - finalMessage = string.Format( - CultureInfo.CurrentCulture, - FrameworkMessages.IsNotInstanceOfFailMsg, - userMessage, - wrongType.ToString(), - value!.GetType().ToString()); + EvidenceBlock evidence = EvidenceBlock.Create() + .AddLine("not expected type:", $"{wrongType} (or derived)") + .AddLine("actual value:", AssertionValueRenderer.RenderValue(value)); + msg.WithEvidence(evidence); } - ReportAssertFailed("Assert.IsNotInstanceOfType", finalMessage); + msg.WithCallSiteExpression(FormatCallSiteExpression("Assert.IsNotInstanceOfType", valueExpression)); + ReportAssertFailed(msg); } } diff --git a/src/TestFramework/TestFramework/Assertions/Assert.cs b/src/TestFramework/TestFramework/Assertions/Assert.cs index 7e565a24b4..19d8c9b4a3 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.cs @@ -165,6 +165,40 @@ internal static void ThrowAssertFailed(StructuredAssertionMessage structuredMess throw CreateAssertFailedException(structuredMessage); } + /// + /// Formats a call-site expression for display at the bottom of a structured assertion message. + /// When the expression is empty or contains newlines, the expression is replaced with a placeholder. + /// + internal static string? FormatCallSiteExpression(string assertionMethodName, string expression) + { + if (string.IsNullOrWhiteSpace(expression)) + { + return null; + } + + // If expression contains newlines (multiline constant), replace with placeholder per RFC + return expression.Contains('\n') || expression.Contains('\r') + ? null + : $"{assertionMethodName}({expression})"; + } + + /// + /// Formats a call-site expression for display at the bottom of a structured assertion message, + /// using two captured expressions. + /// + internal static string? FormatCallSiteExpression(string assertionMethodName, string expression1, string expression2) + { + if (string.IsNullOrWhiteSpace(expression1) || string.IsNullOrWhiteSpace(expression2)) + { + return null; + } + + string arg1 = expression1.Contains('\n') || expression1.Contains('\r') ? "" : expression1; + string arg2 = expression2.Contains('\n') || expression2.Contains('\r') ? "" : expression2; + + return $"{assertionMethodName}({arg1}, {arg2})"; + } + private static string FormatAssertionFailed(string assertionName, string? message) { string failedMessage = string.Format(CultureInfo.CurrentCulture, FrameworkMessages.AssertionFailed, assertionName); diff --git a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.AreSame.cs b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.AreSame.cs index fd7569a444..58fe6db21c 100644 --- a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.AreSame.cs +++ b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.AreSame.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. using AwesomeAssertions; @@ -16,7 +16,8 @@ public void AreSame_PassSameObject_ShouldPass() public void AreSame_PassDifferentObject_ShouldFail() { Action action = () => Assert.AreSame(new object(), new object()); - action.Should().Throw().WithMessage("Assert.AreSame failed. 'expected' expression: 'new object()', 'actual' expression: 'new object()'."); + action.Should().Throw() + .WithMessage($"Assertion failed. Expected both values to refer to the same object.{Environment.NewLine}{Environment.NewLine}expected: System.Object (hash: 0x*){Environment.NewLine}actual: System.Object (hash: 0x*){Environment.NewLine}{Environment.NewLine}Assert.AreSame(new object(), new object())"); } public void AreSame_StringMessage_PassSameObject_ShouldPass() @@ -28,7 +29,8 @@ public void AreSame_StringMessage_PassSameObject_ShouldPass() public void AreSame_StringMessage_PassDifferentObject_ShouldFail() { Action action = () => Assert.AreSame(new object(), new object(), "User-provided message"); - action.Should().Throw().WithMessage("Assert.AreSame failed. 'expected' expression: 'new object()', 'actual' expression: 'new object()'. User-provided message"); + action.Should().Throw() + .WithMessage($"Assertion failed. Expected both values to refer to the same object.{Environment.NewLine}User-provided message{Environment.NewLine}{Environment.NewLine}expected: System.Object (hash: 0x*){Environment.NewLine}actual: System.Object (hash: 0x*){Environment.NewLine}{Environment.NewLine}Assert.AreSame(new object(), new object())"); } public void AreSame_InterpolatedString_PassSameObject_ShouldPass() @@ -43,7 +45,7 @@ public async Task AreSame_InterpolatedString_PassDifferentObject_ShouldFail() DummyClassTrackingToStringCalls o = new(); DateTime dateTime = DateTime.Now; Func action = async () => Assert.AreSame(new object(), new object(), $"User-provided message. {o}, {o,35}, {await GetHelloStringAsync()}, {new DummyIFormattable()}, {dateTime:tt}, {dateTime,5:tt}"); - (await action.Should().ThrowAsync()).WithMessage($"Assert.AreSame failed. 'expected' expression: 'new object()', 'actual' expression: 'new object()'. User-provided message. DummyClassTrackingToStringCalls, DummyClassTrackingToStringCalls, Hello, DummyIFormattable.ToString(), {string.Format(null, "{0:tt}", dateTime)}, {string.Format(null, "{0,5:tt}", dateTime)}"); + (await action.Should().ThrowAsync()).WithMessage($"Assertion failed. Expected both values to refer to the same object.{Environment.NewLine}User-provided message. DummyClassTrackingToStringCalls, DummyClassTrackingToStringCalls, Hello, DummyIFormattable.ToString(), {string.Format(null, "{0:tt}", dateTime)}, {string.Format(null, "{0,5:tt}", dateTime)}{Environment.NewLine}{Environment.NewLine}expected: System.Object (hash: 0x*){Environment.NewLine}actual: System.Object (hash: 0x*){Environment.NewLine}{Environment.NewLine}Assert.AreSame(new object(), new object())"); o.WasToStringCalled.Should().BeTrue(); } @@ -53,68 +55,78 @@ public void AreNotSame_PassDifferentObject_ShouldPass() public void AreSame_BothAreValueTypes_ShouldFailWithSpecializedMessage() { Action action = () => Assert.AreSame(1, 1); - action.Should().Throw().WithMessage("Assert.AreSame failed. Do not pass value types to AreSame(). Values converted to Object will never be the same. Consider using AreEqual(). 'expected' expression: '1', 'actual' expression: '1'."); + action.Should().Throw() + .WithMessage($"Assertion failed. Expected both values to refer to the same object.{Environment.NewLine}Do not pass value types to AreSame \u2014 value types are boxed on each call, so references will never be the same.{Environment.NewLine}{Environment.NewLine}Assert.AreSame(1, 1)"); } public void AreSame_StringMessage_BothAreValueTypes_ShouldFailWithSpecializedMessage() { Action action = () => Assert.AreSame(1, 1, "User-provided message"); - action.Should().Throw().WithMessage("Assert.AreSame failed. Do not pass value types to AreSame(). Values converted to Object will never be the same. Consider using AreEqual(). 'expected' expression: '1', 'actual' expression: '1'. User-provided message"); + action.Should().Throw() + .WithMessage($"Assertion failed. Expected both values to refer to the same object.{Environment.NewLine}Do not pass value types to AreSame \u2014 value types are boxed on each call, so references will never be the same.{Environment.NewLine}User-provided message{Environment.NewLine}{Environment.NewLine}Assert.AreSame(1, 1)"); } public void AreSame_InterpolatedString_BothAreValueTypes_ShouldFailWithSpecializedMessage() { Action action = () => Assert.AreSame(1, 1, $"User-provided message {new object().GetType()}"); - action.Should().Throw().WithMessage("Assert.AreSame failed. Do not pass value types to AreSame(). Values converted to Object will never be the same. Consider using AreEqual(). 'expected' expression: '1', 'actual' expression: '1'. User-provided message System.Object"); + action.Should().Throw() + .WithMessage($"Assertion failed. Expected both values to refer to the same object.{Environment.NewLine}Do not pass value types to AreSame \u2014 value types are boxed on each call, so references will never be the same.{Environment.NewLine}User-provided message System.Object{Environment.NewLine}{Environment.NewLine}Assert.AreSame(1, 1)"); } public void AreSame_ExpectedNull_ShouldFailWithNullMessage() { object? expected = null; Action action = () => Assert.AreSame(expected, new object()); - action.Should().Throw().WithMessage("Assert.AreSame failed. Expected is . 'expected' expression: 'expected', 'actual' expression: 'new object()'."); + action.Should().Throw() + .WithMessage($"Assertion failed. Expected both values to refer to the same object.{Environment.NewLine}{Environment.NewLine}expected: null{Environment.NewLine}actual: System.Object (hash: 0x*){Environment.NewLine}{Environment.NewLine}Assert.AreSame(expected, new object())"); } public void AreSame_ActualNull_ShouldFailWithNullMessage() { object? actual = null; Action action = () => Assert.AreSame(new object(), actual); - action.Should().Throw().WithMessage("Assert.AreSame failed. Actual is . 'expected' expression: 'new object()', 'actual' expression: 'actual'."); + action.Should().Throw() + .WithMessage($"Assertion failed. Expected both values to refer to the same object.{Environment.NewLine}{Environment.NewLine}expected: System.Object (hash: 0x*){Environment.NewLine}actual: null{Environment.NewLine}{Environment.NewLine}Assert.AreSame(new object(), actual)"); } public void AreSame_StringMessage_ExpectedNull_ShouldFailWithNullMessage() { object? expected = null; Action action = () => Assert.AreSame(expected, new object(), "User-provided message"); - action.Should().Throw().WithMessage("Assert.AreSame failed. Expected is . 'expected' expression: 'expected', 'actual' expression: 'new object()'. User-provided message"); + action.Should().Throw() + .WithMessage($"Assertion failed. Expected both values to refer to the same object.{Environment.NewLine}User-provided message{Environment.NewLine}{Environment.NewLine}expected: null{Environment.NewLine}actual: System.Object (hash: 0x*){Environment.NewLine}{Environment.NewLine}Assert.AreSame(expected, new object())"); } public void AreSame_StringMessage_ActualNull_ShouldFailWithNullMessage() { object? actual = null; Action action = () => Assert.AreSame(new object(), actual, "User-provided message"); - action.Should().Throw().WithMessage("Assert.AreSame failed. Actual is . 'expected' expression: 'new object()', 'actual' expression: 'actual'. User-provided message"); + action.Should().Throw() + .WithMessage($"Assertion failed. Expected both values to refer to the same object.{Environment.NewLine}User-provided message{Environment.NewLine}{Environment.NewLine}expected: System.Object (hash: 0x*){Environment.NewLine}actual: null{Environment.NewLine}{Environment.NewLine}Assert.AreSame(new object(), actual)"); } public void AreSame_InterpolatedString_ExpectedNull_ShouldFailWithNullMessage() { object? expected = null; Action action = () => Assert.AreSame(expected, new object(), $"User-provided message {new object().GetType()}"); - action.Should().Throw().WithMessage("Assert.AreSame failed. Expected is . 'expected' expression: 'expected', 'actual' expression: 'new object()'. User-provided message System.Object"); + action.Should().Throw() + .WithMessage($"Assertion failed. Expected both values to refer to the same object.{Environment.NewLine}User-provided message System.Object{Environment.NewLine}{Environment.NewLine}expected: null{Environment.NewLine}actual: System.Object (hash: 0x*){Environment.NewLine}{Environment.NewLine}Assert.AreSame(expected, new object())"); } public void AreSame_InterpolatedString_ActualNull_ShouldFailWithNullMessage() { object? actual = null; Action action = () => Assert.AreSame(new object(), actual, $"User-provided message {new object().GetType()}"); - action.Should().Throw().WithMessage("Assert.AreSame failed. Actual is . 'expected' expression: 'new object()', 'actual' expression: 'actual'. User-provided message System.Object"); + action.Should().Throw() + .WithMessage($"Assertion failed. Expected both values to refer to the same object.{Environment.NewLine}User-provided message System.Object{Environment.NewLine}{Environment.NewLine}expected: System.Object (hash: 0x*){Environment.NewLine}actual: null{Environment.NewLine}{Environment.NewLine}Assert.AreSame(new object(), actual)"); } public void AreNotSame_PassSameObject_ShouldFail() { object o = new(); Action action = () => Assert.AreNotSame(o, o); - action.Should().Throw().WithMessage("Assert.AreNotSame failed. 'notExpected' expression: 'o', 'actual' expression: 'o'."); + action.Should().Throw() + .WithMessage($"Assertion failed. Expected values to refer to different objects.{Environment.NewLine}Both variables refer to the same object.{Environment.NewLine}{Environment.NewLine}Assert.AreNotSame(o, o)"); } public void AreNotSame_StringMessage_PassDifferentObject_ShouldPass() @@ -124,7 +136,8 @@ public void AreNotSame_StringMessage_PassSameObject_ShouldFail() { object o = new(); Action action = () => Assert.AreNotSame(o, o, "User-provided message"); - action.Should().Throw().WithMessage("Assert.AreNotSame failed. 'notExpected' expression: 'o', 'actual' expression: 'o'. User-provided message"); + action.Should().Throw() + .WithMessage($"Assertion failed. Expected values to refer to different objects.{Environment.NewLine}Both variables refer to the same object.{Environment.NewLine}User-provided message{Environment.NewLine}{Environment.NewLine}Assert.AreNotSame(o, o)"); } public void AreNotSame_InterpolatedString_PassDifferentObject_ShouldPass() @@ -139,7 +152,7 @@ public async Task AreNotSame_InterpolatedString_PassSameObject_ShouldFail() DummyClassTrackingToStringCalls o = new(); DateTime dateTime = DateTime.Now; Func action = async () => Assert.AreNotSame(o, o, $"User-provided message. {o}, {o,35}, {await GetHelloStringAsync()}, {new DummyIFormattable()}, {dateTime:tt}, {dateTime,5:tt}"); - (await action.Should().ThrowAsync()).WithMessage($"Assert.AreNotSame failed. 'notExpected' expression: 'o', 'actual' expression: 'o'. User-provided message. DummyClassTrackingToStringCalls, DummyClassTrackingToStringCalls, Hello, DummyIFormattable.ToString(), {string.Format(null, "{0:tt}", dateTime)}, {string.Format(null, "{0,5:tt}", dateTime)}"); + (await action.Should().ThrowAsync()).WithMessage($"Assertion failed. Expected values to refer to different objects.{Environment.NewLine}Both variables refer to the same object.{Environment.NewLine}User-provided message. DummyClassTrackingToStringCalls, DummyClassTrackingToStringCalls, Hello, DummyIFormattable.ToString(), {string.Format(null, "{0:tt}", dateTime)}, {string.Format(null, "{0,5:tt}", dateTime)}{Environment.NewLine}{Environment.NewLine}Assert.AreNotSame(o, o)"); o.WasToStringCalled.Should().BeTrue(); } @@ -148,7 +161,8 @@ public void AreNotSame_BothNull_ShouldFailWithNullMessage() object? notExpected = null; object? actual = null; Action action = () => Assert.AreNotSame(notExpected, actual); - action.Should().Throw().WithMessage("Assert.AreNotSame failed. Both values are . 'notExpected' expression: 'notExpected', 'actual' expression: 'actual'."); + action.Should().Throw() + .WithMessage($"Assertion failed. Expected values to refer to different objects.{Environment.NewLine}Both values are null.{Environment.NewLine}{Environment.NewLine}Assert.AreNotSame(notExpected, actual)"); } public void AreNotSame_StringMessage_BothNull_ShouldFailWithNullMessage() @@ -156,7 +170,8 @@ public void AreNotSame_StringMessage_BothNull_ShouldFailWithNullMessage() object? notExpected = null; object? actual = null; Action action = () => Assert.AreNotSame(notExpected, actual, "User-provided message"); - action.Should().Throw().WithMessage("Assert.AreNotSame failed. Both values are . 'notExpected' expression: 'notExpected', 'actual' expression: 'actual'. User-provided message"); + action.Should().Throw() + .WithMessage($"Assertion failed. Expected values to refer to different objects.{Environment.NewLine}Both values are null.{Environment.NewLine}User-provided message{Environment.NewLine}{Environment.NewLine}Assert.AreNotSame(notExpected, actual)"); } public void AreNotSame_InterpolatedString_BothNull_ShouldFailWithNullMessage() @@ -164,6 +179,7 @@ public void AreNotSame_InterpolatedString_BothNull_ShouldFailWithNullMessage() object? notExpected = null; object? actual = null; Action action = () => Assert.AreNotSame(notExpected, actual, $"User-provided message {new object().GetType()}"); - action.Should().Throw().WithMessage("Assert.AreNotSame failed. Both values are . 'notExpected' expression: 'notExpected', 'actual' expression: 'actual'. User-provided message System.Object"); + action.Should().Throw() + .WithMessage($"Assertion failed. Expected values to refer to different objects.{Environment.NewLine}Both values are null.{Environment.NewLine}User-provided message System.Object{Environment.NewLine}{Environment.NewLine}Assert.AreNotSame(notExpected, actual)"); } } diff --git a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.IsExactInstanceOfTypeTests.cs b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.IsExactInstanceOfTypeTests.cs index aec566af37..e14f3af07c 100644 --- a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.IsExactInstanceOfTypeTests.cs +++ b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.IsExactInstanceOfTypeTests.cs @@ -12,21 +12,21 @@ public void ExactInstanceOfTypeShouldFailWhenValueIsNull() { Action action = () => Assert.IsExactInstanceOfType(null, typeof(AssertTests)); action.Should().Throw() - .WithMessage("Assert.IsExactInstanceOfType failed. 'value' expression: 'null'. Expected exact type:. Actual type:."); + .WithMessage($"Assertion failed. Expected value to be exactly of type AssertTests.{Environment.NewLine}{Environment.NewLine}expected type: Microsoft.VisualStudio.TestPlatform.TestFramework.UnitTests.AssertTests{Environment.NewLine}actual: null{Environment.NewLine}{Environment.NewLine}Assert.IsExactInstanceOfType(null)"); } public void ExactInstanceOfTypeShouldFailWhenTypeIsNull() { Action action = () => Assert.IsExactInstanceOfType(5, null); action.Should().Throw() - .WithMessage("Assert.IsExactInstanceOfType failed. 'value' expression: '5'."); + .WithMessage($"Assertion failed. Expected value to be exactly of type null.{Environment.NewLine}{Environment.NewLine}Assert.IsExactInstanceOfType(5)"); } public void ExactInstanceOfTypeShouldFailWhenTypeIsMismatched() { Action action = () => Assert.IsExactInstanceOfType(5, typeof(string)); action.Should().Throw() - .WithMessage("Assert.IsExactInstanceOfType failed. 'value' expression: '5'. Expected exact type:. Actual type:."); + .WithMessage($"Assertion failed. Expected value to be exactly of type String.{Environment.NewLine}{Environment.NewLine}expected type: System.String{Environment.NewLine}actual type: System.Int32{Environment.NewLine}actual value: 5{Environment.NewLine}{Environment.NewLine}Assert.IsExactInstanceOfType(5)"); } public void ExactInstanceOfTypeShouldPassOnSameInstance() => Assert.IsExactInstanceOfType(5, typeof(int)); @@ -35,7 +35,7 @@ public void ExactInstanceOfTypeShouldFailOnHigherInstance() { Action action = () => Assert.IsExactInstanceOfType(5, typeof(object)); action.Should().Throw() - .WithMessage("Assert.IsExactInstanceOfType failed. 'value' expression: '5'. Expected exact type:. Actual type:."); + .WithMessage($"Assertion failed. Expected value to be exactly of type Object.{Environment.NewLine}{Environment.NewLine}expected type: System.Object{Environment.NewLine}actual type: System.Int32{Environment.NewLine}actual value: 5{Environment.NewLine}{Environment.NewLine}Assert.IsExactInstanceOfType(5)"); } public void ExactInstanceOfTypeShouldFailOnDerivedType() @@ -43,7 +43,7 @@ public void ExactInstanceOfTypeShouldFailOnDerivedType() object x = new MemoryStream(); Action action = () => Assert.IsExactInstanceOfType(x, typeof(Stream)); action.Should().Throw() - .WithMessage("Assert.IsExactInstanceOfType failed. 'value' expression: 'x'. Expected exact type:. Actual type:."); + .WithMessage($"Assertion failed. Expected value to be exactly of type Stream.{Environment.NewLine}{Environment.NewLine}expected type: System.IO.Stream{Environment.NewLine}actual type: System.IO.MemoryStream{Environment.NewLine}actual value: *{Environment.NewLine}{Environment.NewLine}Assert.IsExactInstanceOfType(x)"); } public void ExactInstanceOfTypeShouldPassOnExactType() @@ -56,21 +56,21 @@ public void ExactInstanceOfType_WithStringMessage_ShouldFailWhenValueIsNull() { Action action = () => Assert.IsExactInstanceOfType(null, typeof(AssertTests), "User-provided message"); action.Should().Throw() - .WithMessage("Assert.IsExactInstanceOfType failed. 'value' expression: 'null'. User-provided message Expected exact type:. Actual type:."); + .WithMessage($"Assertion failed. Expected value to be exactly of type AssertTests.{Environment.NewLine}User-provided message{Environment.NewLine}{Environment.NewLine}expected type: Microsoft.VisualStudio.TestPlatform.TestFramework.UnitTests.AssertTests{Environment.NewLine}actual: null{Environment.NewLine}{Environment.NewLine}Assert.IsExactInstanceOfType(null)"); } public void ExactInstanceOfType_WithStringMessage_ShouldFailWhenTypeIsNull() { Action action = () => Assert.IsExactInstanceOfType(5, null, "User-provided message"); action.Should().Throw() - .WithMessage("Assert.IsExactInstanceOfType failed. 'value' expression: '5'. User-provided message"); + .WithMessage($"Assertion failed. Expected value to be exactly of type null.{Environment.NewLine}User-provided message{Environment.NewLine}{Environment.NewLine}Assert.IsExactInstanceOfType(5)"); } public void ExactInstanceOfType_WithStringMessage_ShouldFailWhenTypeIsMismatched() { Action action = () => Assert.IsExactInstanceOfType(5, typeof(string), "User-provided message"); action.Should().Throw() - .WithMessage("Assert.IsExactInstanceOfType failed. 'value' expression: '5'. User-provided message Expected exact type:. Actual type:."); + .WithMessage($"Assertion failed. Expected value to be exactly of type String.{Environment.NewLine}User-provided message{Environment.NewLine}{Environment.NewLine}expected type: System.String{Environment.NewLine}actual type: System.Int32{Environment.NewLine}actual value: 5{Environment.NewLine}{Environment.NewLine}Assert.IsExactInstanceOfType(5)"); } public void ExactInstanceOfType_WithStringMessage_ShouldPassWhenTypeIsCorrect() @@ -82,7 +82,7 @@ public async Task ExactInstanceOfType_WithInterpolatedString_ShouldFailWhenValue DateTime dateTime = DateTime.Now; Func action = async () => Assert.IsExactInstanceOfType(null, typeof(AssertTests), $"User-provided message. {o}, {o,35}, {await GetHelloStringAsync()}, {new DummyIFormattable()}, {dateTime:tt}, {dateTime,5:tt}"); (await action.Should().ThrowAsync()) - .WithMessage($"Assert.IsExactInstanceOfType failed. 'value' expression: 'null'. User-provided message. DummyClassTrackingToStringCalls, DummyClassTrackingToStringCalls, Hello, DummyIFormattable.ToString(), {string.Format(null, "{0:tt}", dateTime)}, {string.Format(null, "{0,5:tt}", dateTime)} Expected exact type:. Actual type:."); + .WithMessage($"Assertion failed. Expected value to be exactly of type AssertTests.{Environment.NewLine}User-provided message. DummyClassTrackingToStringCalls, DummyClassTrackingToStringCalls, Hello, DummyIFormattable.ToString(), {string.Format(null, "{0:tt}", dateTime)}, {string.Format(null, "{0,5:tt}", dateTime)}{Environment.NewLine}{Environment.NewLine}expected type: Microsoft.VisualStudio.TestPlatform.TestFramework.UnitTests.AssertTests{Environment.NewLine}actual: null{Environment.NewLine}{Environment.NewLine}Assert.IsExactInstanceOfType(null)"); o.WasToStringCalled.Should().BeTrue(); } @@ -91,7 +91,7 @@ public void ExactInstanceOfType_WithInterpolatedString_ShouldFailWhenTypeIsNull( DummyClassTrackingToStringCalls o = new(); Action action = () => Assert.IsExactInstanceOfType(5, null, $"User-provided message {o}"); action.Should().Throw() - .WithMessage("Assert.IsExactInstanceOfType failed. 'value' expression: '5'. User-provided message DummyClassTrackingToStringCalls"); + .WithMessage($"Assertion failed. Expected value to be exactly of type null.{Environment.NewLine}User-provided message DummyClassTrackingToStringCalls{Environment.NewLine}{Environment.NewLine}Assert.IsExactInstanceOfType(5)"); o.WasToStringCalled.Should().BeTrue(); } @@ -100,7 +100,7 @@ public void ExactInstanceOfType_WithInterpolatedString_ShouldFailWhenTypeIsMisma DummyClassTrackingToStringCalls o = new(); Action action = () => Assert.IsExactInstanceOfType(5, typeof(string), $"User-provided message {o}"); action.Should().Throw() - .WithMessage("Assert.IsExactInstanceOfType failed. 'value' expression: '5'. User-provided message DummyClassTrackingToStringCalls Expected exact type:. Actual type:."); + .WithMessage($"Assertion failed. Expected value to be exactly of type String.{Environment.NewLine}User-provided message DummyClassTrackingToStringCalls{Environment.NewLine}{Environment.NewLine}expected type: System.String{Environment.NewLine}actual type: System.Int32{Environment.NewLine}actual value: 5{Environment.NewLine}{Environment.NewLine}Assert.IsExactInstanceOfType(5)"); o.WasToStringCalled.Should().BeTrue(); } @@ -134,21 +134,21 @@ public void ExactInstanceNotOfTypeShouldFailOnExactType() object x = new MemoryStream(); Action action = () => Assert.IsNotExactInstanceOfType(x, typeof(MemoryStream)); action.Should().Throw() - .WithMessage("Assert.IsNotExactInstanceOfType failed. Wrong exact Type:. Actual type:. 'value' expression: 'x'."); + .WithMessage($"Assertion failed. Expected value to not be exactly of type MemoryStream.{Environment.NewLine}{Environment.NewLine}not expected type: System.IO.MemoryStream{Environment.NewLine}actual value: *{Environment.NewLine}{Environment.NewLine}Assert.IsNotExactInstanceOfType(x)"); } public void IsExactInstanceOfTypeUsingGenericType_WhenValueIsNull_Fails() { Action action = () => Assert.IsExactInstanceOfType(null); action.Should().Throw() - .WithMessage("Assert.IsExactInstanceOfType failed. 'value' expression: 'null'. Expected exact type:. Actual type:."); + .WithMessage($"Assertion failed. Expected value to be exactly of type AssertTests.{Environment.NewLine}{Environment.NewLine}expected type: Microsoft.VisualStudio.TestPlatform.TestFramework.UnitTests.AssertTests{Environment.NewLine}actual: null{Environment.NewLine}{Environment.NewLine}Assert.IsExactInstanceOfType(null)"); } public void IsExactInstanceOfTypeUsingGenericType_WhenTypeMismatch_Fails() { Action action = () => Assert.IsExactInstanceOfType(5); action.Should().Throw() - .WithMessage("Assert.IsExactInstanceOfType failed. 'value' expression: '5'. Expected exact type:. Actual type:."); + .WithMessage($"Assertion failed. Expected value to be exactly of type String.{Environment.NewLine}{Environment.NewLine}expected type: System.String{Environment.NewLine}actual type: System.Int32{Environment.NewLine}actual value: 5{Environment.NewLine}{Environment.NewLine}Assert.IsExactInstanceOfType(5)"); } public void IsExactInstanceOfTypeUsingGenericType_WhenDerivedType_Fails() @@ -156,7 +156,7 @@ public void IsExactInstanceOfTypeUsingGenericType_WhenDerivedType_Fails() object x = new MemoryStream(); Action action = () => Assert.IsExactInstanceOfType(x); action.Should().Throw() - .WithMessage("Assert.IsExactInstanceOfType failed. 'value' expression: 'x'. Expected exact type:. Actual type:."); + .WithMessage($"Assertion failed. Expected value to be exactly of type Stream.{Environment.NewLine}{Environment.NewLine}expected type: System.IO.Stream{Environment.NewLine}actual type: System.IO.MemoryStream{Environment.NewLine}actual value: *{Environment.NewLine}{Environment.NewLine}Assert.IsExactInstanceOfType(x)"); } public void IsExactInstanceOfTypeUsingGenericType_OnSameInstance_DoesNotThrow() => Assert.IsExactInstanceOfType(5); @@ -178,7 +178,7 @@ public void IsExactInstanceOfTypeUsingGenericType_OnHigherInstance_Fails() { Action action = () => Assert.IsExactInstanceOfType(5); action.Should().Throw() - .WithMessage("Assert.IsExactInstanceOfType failed. 'value' expression: '5'. Expected exact type:. Actual type:."); + .WithMessage($"Assertion failed. Expected value to be exactly of type Object.{Environment.NewLine}{Environment.NewLine}expected type: System.Object{Environment.NewLine}actual type: System.Int32{Environment.NewLine}actual value: 5{Environment.NewLine}{Environment.NewLine}Assert.IsExactInstanceOfType(5)"); } public void IsExactInstanceOfTypeUsingGenericTypeWithReturn_OnExactType_DoesNotThrow() @@ -205,7 +205,7 @@ public void IsNotExactInstanceOfTypeUsingGenericType_OnExactType_Fails() object x = new MemoryStream(); Action action = () => Assert.IsNotExactInstanceOfType(x); action.Should().Throw() - .WithMessage("Assert.IsNotExactInstanceOfType failed. Wrong exact Type:. Actual type:. 'value' expression: 'x'."); + .WithMessage($"Assertion failed. Expected value to not be exactly of type MemoryStream.{Environment.NewLine}{Environment.NewLine}not expected type: System.IO.MemoryStream{Environment.NewLine}actual value: *{Environment.NewLine}{Environment.NewLine}Assert.IsNotExactInstanceOfType(x)"); } public void IsExactInstanceOfType_WhenNonNullNullableValue_LearnNonNull() diff --git a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.IsInstanceOfTypeTests.cs b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.IsInstanceOfTypeTests.cs index 8dbe699579..9ddd682da6 100644 --- a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.IsInstanceOfTypeTests.cs +++ b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.IsInstanceOfTypeTests.cs @@ -12,21 +12,21 @@ public void InstanceOfTypeShouldFailWhenValueIsNull() { Action action = () => Assert.IsInstanceOfType(null, typeof(AssertTests)); action.Should().Throw() - .WithMessage("Assert.IsInstanceOfType failed. 'value' expression: 'null'. Expected type:. Actual type:."); + .WithMessage($"Assertion failed. Expected value to be of type AssertTests (or derived).{Environment.NewLine}{Environment.NewLine}expected type: Microsoft.VisualStudio.TestPlatform.TestFramework.UnitTests.AssertTests (or derived){Environment.NewLine}actual: null{Environment.NewLine}{Environment.NewLine}Assert.IsInstanceOfType(null)"); } public void InstanceOfTypeShouldFailWhenTypeIsNull() { Action action = () => Assert.IsInstanceOfType(5, null); action.Should().Throw() - .WithMessage("Assert.IsInstanceOfType failed. 'value' expression: '5'."); + .WithMessage($"Assertion failed. Expected value to be of type null (or derived).{Environment.NewLine}{Environment.NewLine}Assert.IsInstanceOfType(5)"); } public void InstanceOfTypeShouldFailWhenTypeIsMismatched() { Action action = () => Assert.IsInstanceOfType(5, typeof(string)); action.Should().Throw() - .WithMessage("Assert.IsInstanceOfType failed. 'value' expression: '5'. Expected type:. Actual type:."); + .WithMessage($"Assertion failed. Expected value to be of type String (or derived).{Environment.NewLine}{Environment.NewLine}expected type: System.String (or derived){Environment.NewLine}actual type: System.Int32{Environment.NewLine}actual value: 5{Environment.NewLine}{Environment.NewLine}Assert.IsInstanceOfType(5)"); } public void InstanceOfTypeShouldPassOnSameInstance() => Assert.IsInstanceOfType(5, typeof(int)); @@ -37,21 +37,21 @@ public void InstanceOfType_WithStringMessage_ShouldFailWhenValueIsNull() { Action action = () => Assert.IsInstanceOfType(null, typeof(AssertTests), "User-provided message"); action.Should().Throw() - .WithMessage("Assert.IsInstanceOfType failed. 'value' expression: 'null'. User-provided message Expected type:. Actual type:."); + .WithMessage($"Assertion failed. Expected value to be of type AssertTests (or derived).{Environment.NewLine}User-provided message{Environment.NewLine}{Environment.NewLine}expected type: Microsoft.VisualStudio.TestPlatform.TestFramework.UnitTests.AssertTests (or derived){Environment.NewLine}actual: null{Environment.NewLine}{Environment.NewLine}Assert.IsInstanceOfType(null)"); } public void InstanceOfType_WithStringMessage_ShouldFailWhenTypeIsNull() { Action action = () => Assert.IsInstanceOfType(5, null, "User-provided message"); action.Should().Throw() - .WithMessage("Assert.IsInstanceOfType failed. 'value' expression: '5'. User-provided message"); + .WithMessage($"Assertion failed. Expected value to be of type null (or derived).{Environment.NewLine}User-provided message{Environment.NewLine}{Environment.NewLine}Assert.IsInstanceOfType(5)"); } public void InstanceOfType_WithStringMessage_ShouldFailWhenTypeIsMismatched() { Action action = () => Assert.IsInstanceOfType(5, typeof(string), "User-provided message"); action.Should().Throw() - .WithMessage("Assert.IsInstanceOfType failed. 'value' expression: '5'. User-provided message Expected type:. Actual type:."); + .WithMessage($"Assertion failed. Expected value to be of type String (or derived).{Environment.NewLine}User-provided message{Environment.NewLine}{Environment.NewLine}expected type: System.String (or derived){Environment.NewLine}actual type: System.Int32{Environment.NewLine}actual value: 5{Environment.NewLine}{Environment.NewLine}Assert.IsInstanceOfType(5)"); } public void InstanceOfType_WithStringMessage_ShouldPassWhenTypeIsCorrect() @@ -63,7 +63,7 @@ public async Task InstanceOfType_WithInterpolatedString_ShouldFailWhenValueIsNul DateTime dateTime = DateTime.Now; Func action = async () => Assert.IsInstanceOfType(null, typeof(AssertTests), $"User-provided message. {o}, {o,35}, {await GetHelloStringAsync()}, {new DummyIFormattable()}, {dateTime:tt}, {dateTime,5:tt}"); (await action.Should().ThrowAsync()) - .WithMessage($"Assert.IsInstanceOfType failed. 'value' expression: 'null'. User-provided message. DummyClassTrackingToStringCalls, DummyClassTrackingToStringCalls, Hello, DummyIFormattable.ToString(), {string.Format(null, "{0:tt}", dateTime)}, {string.Format(null, "{0,5:tt}", dateTime)} Expected type:. Actual type:."); + .WithMessage($"Assertion failed. Expected value to be of type AssertTests (or derived).{Environment.NewLine}User-provided message. DummyClassTrackingToStringCalls, DummyClassTrackingToStringCalls, Hello, DummyIFormattable.ToString(), {string.Format(null, "{0:tt}", dateTime)}, {string.Format(null, "{0,5:tt}", dateTime)}{Environment.NewLine}{Environment.NewLine}expected type: Microsoft.VisualStudio.TestPlatform.TestFramework.UnitTests.AssertTests (or derived){Environment.NewLine}actual: null{Environment.NewLine}{Environment.NewLine}Assert.IsInstanceOfType(null)"); o.WasToStringCalled.Should().BeTrue(); } @@ -72,7 +72,7 @@ public void InstanceOfType_WithInterpolatedString_ShouldFailWhenTypeIsNull() DummyClassTrackingToStringCalls o = new(); Action action = () => Assert.IsInstanceOfType(5, null, $"User-provided message {o}"); action.Should().Throw() - .WithMessage("Assert.IsInstanceOfType failed. 'value' expression: '5'. User-provided message DummyClassTrackingToStringCalls"); + .WithMessage($"Assertion failed. Expected value to be of type null (or derived).{Environment.NewLine}User-provided message DummyClassTrackingToStringCalls{Environment.NewLine}{Environment.NewLine}Assert.IsInstanceOfType(5)"); o.WasToStringCalled.Should().BeTrue(); } @@ -81,7 +81,7 @@ public void InstanceOfType_WithInterpolatedString_ShouldFailWhenTypeIsMismatched DummyClassTrackingToStringCalls o = new(); Action action = () => Assert.IsInstanceOfType(5, typeof(string), $"User-provided message {o}"); action.Should().Throw() - .WithMessage("Assert.IsInstanceOfType failed. 'value' expression: '5'. User-provided message DummyClassTrackingToStringCalls Expected type:. Actual type:."); + .WithMessage($"Assertion failed. Expected value to be of type String (or derived).{Environment.NewLine}User-provided message DummyClassTrackingToStringCalls{Environment.NewLine}{Environment.NewLine}expected type: System.String (or derived){Environment.NewLine}actual type: System.Int32{Environment.NewLine}actual value: 5{Environment.NewLine}{Environment.NewLine}Assert.IsInstanceOfType(5)"); o.WasToStringCalled.Should().BeTrue(); } @@ -108,14 +108,14 @@ public void IsInstanceOfTypeUsingGenericType_WhenValueIsNull_Fails() { Action action = () => Assert.IsInstanceOfType(null); action.Should().Throw() - .WithMessage("Assert.IsInstanceOfType failed. 'value' expression: 'null'. Expected type:. Actual type:."); + .WithMessage($"Assertion failed. Expected value to be of type AssertTests (or derived).{Environment.NewLine}{Environment.NewLine}expected type: Microsoft.VisualStudio.TestPlatform.TestFramework.UnitTests.AssertTests (or derived){Environment.NewLine}actual: null{Environment.NewLine}{Environment.NewLine}Assert.IsInstanceOfType(null)"); } public void IsInstanceOfTypeUsingGenericType_WhenTypeMismatch_Fails() { Action action = () => Assert.IsInstanceOfType(5); action.Should().Throw() - .WithMessage("Assert.IsInstanceOfType failed. 'value' expression: '5'. Expected type:. Actual type:."); + .WithMessage($"Assertion failed. Expected value to be of type String (or derived).{Environment.NewLine}{Environment.NewLine}expected type: System.String (or derived){Environment.NewLine}actual type: System.Int32{Environment.NewLine}actual value: 5{Environment.NewLine}{Environment.NewLine}Assert.IsInstanceOfType(5)"); } public void IsInstanceOfTypeUsingGenericTypeWithOutParameter_WhenValueIsNull_Fails() diff --git a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.ScopeTests.cs b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.ScopeTests.cs index 096dbb9905..9cbebf1ce4 100644 --- a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.ScopeTests.cs +++ b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.ScopeTests.cs @@ -160,7 +160,7 @@ public void Scope_AssertIsInstanceOfType_IsSoftFailure() .Which; innerException.InnerExceptions.Should().HaveCount(2); - innerException.InnerExceptions[0].Message.Should().Be("Assert.IsInstanceOfType failed. 'value' expression: 'value'. Expected type:. Actual type:."); + innerException.InnerExceptions[0].Message.Should().Contain("Assertion failed. Expected value to be of type Int32 (or derived)."); innerException.InnerExceptions[1].Message.Should().Contain("Assert.AreEqual failed."); } @@ -183,7 +183,7 @@ public void Scope_AssertIsExactInstanceOfType_IsSoftFailure() .Which; innerException.InnerExceptions.Should().HaveCount(2); - innerException.InnerExceptions[0].Message.Should().Be("Assert.IsExactInstanceOfType failed. 'value' expression: 'value'. Expected exact type:. Actual type:."); + innerException.InnerExceptions[0].Message.Should().Contain("Assertion failed. Expected value to be exactly of type Object."); innerException.InnerExceptions[1].Message.Should().Contain("Assert.AreEqual failed."); } From 2777caeeaeea387ca74ef1c4017ae7460be839ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Thu, 14 May 2026 11:34:10 +0200 Subject: [PATCH 3/5] Address expert review feedback for structured assertion changes - Make FormatCallSiteExpression placeholders caller-controlled (H1, M3) so AreNotSame, IsInstanceOfType, etc. show meaningful parameter names instead of misleading / defaults.- Guard AssertionValueRenderer against user ToString() exceptions (H3) so a faulty ToString in a tested type does not mask the original assertion failure.- Populate WithExpectedAndActual on IsInstanceOfType / IsExactInstanceOfType (M5) to surface the IDE diff payload consistently with AreSame.- Strengthen Scope tests for type assertions (M7) by additionally asserting evidence-block lines and call-site expression.- Strip spurious BOM accidentally added to Assert.IsInstanceOfType.cs and Assert.IsExactInstanceOfType.cs (N1). --- .../Assertions/Assert.AreSame.cs | 4 ++-- .../Assert.IsExactInstanceOfType.cs | 7 ++++--- .../Assertions/Assert.IsInstanceOfType.cs | 7 ++++--- .../TestFramework/Assertions/Assert.cs | 19 +++++++++---------- .../Assertions/AssertionValueRenderer.cs | 15 ++++++++++++++- .../Assertions/AssertTests.ScopeTests.cs | 12 ++++++++++-- 6 files changed, 43 insertions(+), 21 deletions(-) diff --git a/src/TestFramework/TestFramework/Assertions/Assert.AreSame.cs b/src/TestFramework/TestFramework/Assertions/Assert.AreSame.cs index 9de25e7d0c..d072c4de25 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.AreSame.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.AreSame.cs @@ -207,7 +207,7 @@ private static void ReportAssertAreSameFailed(T? expected, T? actual, string? msg.WithEvidence(evidence).WithExpectedAndActual(expectedText, actualText); } - msg.WithCallSiteExpression(FormatCallSiteExpression("Assert.AreSame", expectedExpression, actualExpression)); + msg.WithCallSiteExpression(FormatCallSiteExpression("Assert.AreSame", expectedExpression, actualExpression, "", "")); ReportAssertFailed(msg); } @@ -272,7 +272,7 @@ notExpected is null && actual is null msg.WithUserMessage(userMessage); - msg.WithCallSiteExpression(FormatCallSiteExpression("Assert.AreNotSame", notExpectedExpression, actualExpression)); + msg.WithCallSiteExpression(FormatCallSiteExpression("Assert.AreNotSame", notExpectedExpression, actualExpression, "", "")); ReportAssertFailed(msg); } diff --git a/src/TestFramework/TestFramework/Assertions/Assert.IsExactInstanceOfType.cs b/src/TestFramework/TestFramework/Assertions/Assert.IsExactInstanceOfType.cs index 59f78089a4..09941e3ce4 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.IsExactInstanceOfType.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.IsExactInstanceOfType.cs @@ -345,10 +345,11 @@ private static void ReportAssertIsExactInstanceOfTypeFailed(object? value, Type? evidence.AddLine("actual value:", AssertionValueRenderer.RenderValue(value)); } - msg.WithEvidence(evidence); + msg.WithEvidence(evidence) + .WithExpectedAndActual(expectedType.ToString(), value?.GetType().ToString() ?? "null"); } - msg.WithCallSiteExpression(FormatCallSiteExpression("Assert.IsExactInstanceOfType", valueExpression)); + msg.WithCallSiteExpression(FormatCallSiteExpression("Assert.IsExactInstanceOfType", valueExpression, "")); ReportAssertFailed(msg); } @@ -426,7 +427,7 @@ private static void ReportAssertIsNotExactInstanceOfTypeFailed(object? value, Ty msg.WithEvidence(evidence); } - msg.WithCallSiteExpression(FormatCallSiteExpression("Assert.IsNotExactInstanceOfType", valueExpression)); + msg.WithCallSiteExpression(FormatCallSiteExpression("Assert.IsNotExactInstanceOfType", valueExpression, "")); ReportAssertFailed(msg); } } diff --git a/src/TestFramework/TestFramework/Assertions/Assert.IsInstanceOfType.cs b/src/TestFramework/TestFramework/Assertions/Assert.IsInstanceOfType.cs index e11b00fadd..bf6d4e34a0 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.IsInstanceOfType.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.IsInstanceOfType.cs @@ -347,10 +347,11 @@ private static void ReportAssertIsInstanceOfTypeFailed(object? value, Type? expe evidence.AddLine("actual value:", AssertionValueRenderer.RenderValue(value)); } - msg.WithEvidence(evidence); + msg.WithEvidence(evidence) + .WithExpectedAndActual($"{expectedType} (or derived)", value?.GetType().ToString() ?? "null"); } - msg.WithCallSiteExpression(FormatCallSiteExpression("Assert.IsInstanceOfType", valueExpression)); + msg.WithCallSiteExpression(FormatCallSiteExpression("Assert.IsInstanceOfType", valueExpression, "")); ReportAssertFailed(msg); } @@ -430,7 +431,7 @@ private static void ReportAssertIsNotInstanceOfTypeFailed(object? value, Type? w msg.WithEvidence(evidence); } - msg.WithCallSiteExpression(FormatCallSiteExpression("Assert.IsNotInstanceOfType", valueExpression)); + msg.WithCallSiteExpression(FormatCallSiteExpression("Assert.IsNotInstanceOfType", valueExpression, "")); ReportAssertFailed(msg); } } diff --git a/src/TestFramework/TestFramework/Assertions/Assert.cs b/src/TestFramework/TestFramework/Assertions/Assert.cs index 493bac2bee..4c7b540251 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.cs @@ -178,34 +178,33 @@ internal static void ThrowAssertFailed(StructuredAssertionMessage structuredMess /// /// Formats a call-site expression for display at the bottom of a structured assertion message. - /// When the expression is empty or contains newlines, the expression is replaced with a placeholder. + /// When the expression contains newlines (multiline constant), it is replaced with the supplied placeholder. + /// Returns when the expression is empty or whitespace. /// - internal static string? FormatCallSiteExpression(string assertionMethodName, string expression) + internal static string? FormatCallSiteExpression(string assertionMethodName, string expression, string placeholder = "") { if (string.IsNullOrWhiteSpace(expression)) { return null; } - // If expression contains newlines (multiline constant), replace with placeholder per RFC - return expression.Contains('\n') || expression.Contains('\r') - ? null - : $"{assertionMethodName}({expression})"; + string arg = expression.Contains('\n') || expression.Contains('\r') ? placeholder : expression; + return $"{assertionMethodName}({arg})"; } /// /// Formats a call-site expression for display at the bottom of a structured assertion message, - /// using two captured expressions. + /// using two captured expressions. Multiline expressions are replaced with the supplied placeholders. /// - internal static string? FormatCallSiteExpression(string assertionMethodName, string expression1, string expression2) + internal static string? FormatCallSiteExpression(string assertionMethodName, string expression1, string expression2, string placeholder1 = "", string placeholder2 = "") { if (string.IsNullOrWhiteSpace(expression1) || string.IsNullOrWhiteSpace(expression2)) { return null; } - string arg1 = expression1.Contains('\n') || expression1.Contains('\r') ? "" : expression1; - string arg2 = expression2.Contains('\n') || expression2.Contains('\r') ? "" : expression2; + string arg1 = expression1.Contains('\n') || expression1.Contains('\r') ? placeholder1 : expression1; + string arg2 = expression2.Contains('\n') || expression2.Contains('\r') ? placeholder2 : expression2; return $"{assertionMethodName}({arg1}, {arg2})"; } diff --git a/src/TestFramework/TestFramework/Assertions/AssertionValueRenderer.cs b/src/TestFramework/TestFramework/Assertions/AssertionValueRenderer.cs index e4a40a153c..fd34336624 100644 --- a/src/TestFramework/TestFramework/Assertions/AssertionValueRenderer.cs +++ b/src/TestFramework/TestFramework/Assertions/AssertionValueRenderer.cs @@ -19,9 +19,22 @@ internal static string RenderValue(object? value) bool b => b ? "true" : "false", char c => RenderChar(c), IEnumerable enumerable => RenderCollection(enumerable), - _ => value.ToString() ?? value.GetType().FullName ?? value.GetType().Name, + _ => RenderObject(value), }; + private static string RenderObject(object value) + { + // Guard against user types whose ToString() throws so the original assertion failure is preserved. + try + { + return value.ToString() ?? value.GetType().FullName ?? value.GetType().Name; + } + catch (Exception ex) + { + return $"{value.GetType().FullName ?? value.GetType().Name} (ToString threw {ex.GetType().Name})"; + } + } + /// /// Renders a string value with double quotes and escape sequences for control characters. /// diff --git a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.ScopeTests.cs b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.ScopeTests.cs index 9cbebf1ce4..b5c70a251b 100644 --- a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.ScopeTests.cs +++ b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.ScopeTests.cs @@ -160,7 +160,11 @@ public void Scope_AssertIsInstanceOfType_IsSoftFailure() .Which; innerException.InnerExceptions.Should().HaveCount(2); - innerException.InnerExceptions[0].Message.Should().Contain("Assertion failed. Expected value to be of type Int32 (or derived)."); + string instanceOfTypeMessage = innerException.InnerExceptions[0].Message; + instanceOfTypeMessage.Should().Contain("Assertion failed. Expected value to be of type Int32 (or derived)."); + instanceOfTypeMessage.Should().Contain("expected type: System.Int32 (or derived)"); + instanceOfTypeMessage.Should().Contain("actual type: System.String"); + instanceOfTypeMessage.Should().Contain("Assert.IsInstanceOfType(value, typeof(int))"); innerException.InnerExceptions[1].Message.Should().Contain("Assert.AreEqual failed."); } @@ -183,7 +187,11 @@ public void Scope_AssertIsExactInstanceOfType_IsSoftFailure() .Which; innerException.InnerExceptions.Should().HaveCount(2); - innerException.InnerExceptions[0].Message.Should().Contain("Assertion failed. Expected value to be exactly of type Object."); + string exactInstanceOfTypeMessage = innerException.InnerExceptions[0].Message; + exactInstanceOfTypeMessage.Should().Contain("Assertion failed. Expected value to be exactly of type Object."); + exactInstanceOfTypeMessage.Should().Contain("expected type: System.Object"); + exactInstanceOfTypeMessage.Should().Contain("actual type: System.String"); + exactInstanceOfTypeMessage.Should().Contain("Assert.IsExactInstanceOfType(value, typeof(object))"); innerException.InnerExceptions[1].Message.Should().Contain("Assert.AreEqual failed."); } From 4a3cd8b4a89a2654aa5be648a912db59a981a9b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Thu, 14 May 2026 11:42:58 +0200 Subject: [PATCH 4/5] fixup: correct call-site expectations in scope tests IsInstanceOfType / IsExactInstanceOfType only capture the 'value' argument via CallerArgumentExpression, so the call-site shows only Assert.X(value), not Assert.X(value, typeof(...)). --- .../Assertions/AssertTests.ScopeTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.ScopeTests.cs b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.ScopeTests.cs index b5c70a251b..adc4ae9b12 100644 --- a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.ScopeTests.cs +++ b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.ScopeTests.cs @@ -164,7 +164,7 @@ public void Scope_AssertIsInstanceOfType_IsSoftFailure() instanceOfTypeMessage.Should().Contain("Assertion failed. Expected value to be of type Int32 (or derived)."); instanceOfTypeMessage.Should().Contain("expected type: System.Int32 (or derived)"); instanceOfTypeMessage.Should().Contain("actual type: System.String"); - instanceOfTypeMessage.Should().Contain("Assert.IsInstanceOfType(value, typeof(int))"); + instanceOfTypeMessage.Should().Contain("Assert.IsInstanceOfType(value)"); innerException.InnerExceptions[1].Message.Should().Contain("Assert.AreEqual failed."); } @@ -191,7 +191,7 @@ public void Scope_AssertIsExactInstanceOfType_IsSoftFailure() exactInstanceOfTypeMessage.Should().Contain("Assertion failed. Expected value to be exactly of type Object."); exactInstanceOfTypeMessage.Should().Contain("expected type: System.Object"); exactInstanceOfTypeMessage.Should().Contain("actual type: System.String"); - exactInstanceOfTypeMessage.Should().Contain("Assert.IsExactInstanceOfType(value, typeof(object))"); + exactInstanceOfTypeMessage.Should().Contain("Assert.IsExactInstanceOfType(value)"); innerException.InnerExceptions[1].Message.Should().Contain("Assert.AreEqual failed."); } From 28bdc80179ebf5ffb6ebaf2e11d8a91b1d85f750 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Thu, 14 May 2026 14:31:21 +0200 Subject: [PATCH 5/5] Address review comments: payload on negative type assertions, raw-string tests, full-equality assertions - Add WithExpectedAndActual payload to IsNotInstanceOfType / IsNotExactInstanceOfType failures so ExpectedText/ActualText/Data are populated for the negative paths. - Convert all type-assertion tests to use C# raw string literals instead of Environment.NewLine interpolations and update for new message format (null-type guard, dropped 'actual value:', added 'actual type:' on negative paths, 'Both values' wording). - Replace .Should().Contain(...) checks in scope soft-failure tests with full-equality .Should().Be(rawString) assertions. - Add ObjectWithThrowingToString test for AssertionValueRenderer.RenderObject ToString-throws fallback. - Add new AssertTests.FormatCallSiteExpressionTests covering null/empty/multiline behavior on both single- and two-arg overloads (uses named arguments to disambiguate overloads). --- .../Assertions/Assert.AreSame.cs | 2 +- .../Assert.IsExactInstanceOfType.cs | 32 ++- .../Assertions/Assert.IsInstanceOfType.cs | 32 ++- .../TestFramework/Assertions/Assert.cs | 16 +- .../Assertions/AssertTests.AreSame.cs | 187 ++++++++++++++-- ...sertTests.FormatCallSiteExpressionTests.cs | 46 ++++ .../AssertTests.IsExactInstanceOfTypeTests.cs | 204 ++++++++++++++++-- .../AssertTests.IsInstanceOfTypeTests.cs | 186 +++++++++++++++- .../Assertions/AssertTests.ScopeTests.cs | 28 ++- .../Assertions/AssertionValueRendererTests.cs | 9 + 10 files changed, 645 insertions(+), 97 deletions(-) create mode 100644 test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.FormatCallSiteExpressionTests.cs diff --git a/src/TestFramework/TestFramework/Assertions/Assert.AreSame.cs b/src/TestFramework/TestFramework/Assertions/Assert.AreSame.cs index d072c4de25..dced467094 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.AreSame.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.AreSame.cs @@ -268,7 +268,7 @@ private static void ReportAssertAreNotSameFailed(T? notExpected, T? actual, s msg.WithAdditionalSummaryLine( notExpected is null && actual is null ? "Both values are null." - : "Both variables refer to the same object."); + : "Both values refer to the same object."); msg.WithUserMessage(userMessage); diff --git a/src/TestFramework/TestFramework/Assertions/Assert.IsExactInstanceOfType.cs b/src/TestFramework/TestFramework/Assertions/Assert.IsExactInstanceOfType.cs index 09941e3ce4..7961cda03e 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.IsExactInstanceOfType.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.IsExactInstanceOfType.cs @@ -327,26 +327,19 @@ private static bool IsExactInstanceOfTypeFailing([NotNullWhen(false)] object? va [DoesNotReturn] private static void ReportAssertIsExactInstanceOfTypeFailed(object? value, Type? expectedType, string? userMessage, string valueExpression) { - string typeName = expectedType?.Name ?? "null"; - StructuredAssertionMessage msg = new($"Expected value to be exactly of type {typeName}."); + StructuredAssertionMessage msg = expectedType is null + ? new("Cannot check type because the expected type argument is null.") + : new($"Expected value to be exactly of type {expectedType.Name}."); msg.WithUserMessage(userMessage); if (expectedType is not null) { + string actualTypeText = value?.GetType().ToString() ?? "null"; EvidenceBlock evidence = EvidenceBlock.Create() - .AddLine("expected type:", expectedType.ToString()); - if (value is null) - { - evidence.AddLine("actual:", "null"); - } - else - { - evidence.AddLine("actual type:", value.GetType().ToString()); - evidence.AddLine("actual value:", AssertionValueRenderer.RenderValue(value)); - } - + .AddLine("expected type:", expectedType.ToString()) + .AddLine(value is null ? "actual:" : "actual type:", actualTypeText); msg.WithEvidence(evidence) - .WithExpectedAndActual(expectedType.ToString(), value?.GetType().ToString() ?? "null"); + .WithExpectedAndActual(expectedType.ToString(), actualTypeText); } msg.WithCallSiteExpression(FormatCallSiteExpression("Assert.IsExactInstanceOfType", valueExpression, "")); @@ -415,16 +408,19 @@ private static bool IsNotExactInstanceOfTypeFailing(object? value, [NotNullWhen( [DoesNotReturn] private static void ReportAssertIsNotExactInstanceOfTypeFailed(object? value, Type? wrongType, string? userMessage, string valueExpression) { - string typeName = wrongType?.Name ?? "null"; - StructuredAssertionMessage msg = new($"Expected value to not be exactly of type {typeName}."); + StructuredAssertionMessage msg = wrongType is null + ? new("Cannot check type because the not-expected type argument is null.") + : new($"Expected value to not be exactly of type {wrongType.Name}."); msg.WithUserMessage(userMessage); if (wrongType is not null) { + string actualTypeText = value?.GetType().ToString() ?? "null"; EvidenceBlock evidence = EvidenceBlock.Create() .AddLine("not expected type:", wrongType.ToString()) - .AddLine("actual value:", AssertionValueRenderer.RenderValue(value)); - msg.WithEvidence(evidence); + .AddLine("actual type:", actualTypeText); + msg.WithEvidence(evidence) + .WithExpectedAndActual(wrongType.ToString(), actualTypeText); } msg.WithCallSiteExpression(FormatCallSiteExpression("Assert.IsNotExactInstanceOfType", valueExpression, "")); diff --git a/src/TestFramework/TestFramework/Assertions/Assert.IsInstanceOfType.cs b/src/TestFramework/TestFramework/Assertions/Assert.IsInstanceOfType.cs index bf6d4e34a0..a9a9231217 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.IsInstanceOfType.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.IsInstanceOfType.cs @@ -329,26 +329,19 @@ private static bool IsInstanceOfTypeFailing([NotNullWhen(false)] object? value, [DoesNotReturn] private static void ReportAssertIsInstanceOfTypeFailed(object? value, Type? expectedType, string? userMessage, string valueExpression) { - string typeName = expectedType?.Name ?? "null"; - StructuredAssertionMessage msg = new($"Expected value to be of type {typeName} (or derived)."); + StructuredAssertionMessage msg = expectedType is null + ? new("Cannot check type because the expected type argument is null.") + : new($"Expected value to be of type {expectedType.Name} (or derived)."); msg.WithUserMessage(userMessage); if (expectedType is not null) { + string actualTypeText = value?.GetType().ToString() ?? "null"; EvidenceBlock evidence = EvidenceBlock.Create() - .AddLine("expected type:", $"{expectedType} (or derived)"); - if (value is null) - { - evidence.AddLine("actual:", "null"); - } - else - { - evidence.AddLine("actual type:", value.GetType().ToString()); - evidence.AddLine("actual value:", AssertionValueRenderer.RenderValue(value)); - } - + .AddLine("expected type:", $"{expectedType} (or derived)") + .AddLine(value is null ? "actual:" : "actual type:", actualTypeText); msg.WithEvidence(evidence) - .WithExpectedAndActual($"{expectedType} (or derived)", value?.GetType().ToString() ?? "null"); + .WithExpectedAndActual($"{expectedType} (or derived)", actualTypeText); } msg.WithCallSiteExpression(FormatCallSiteExpression("Assert.IsInstanceOfType", valueExpression, "")); @@ -419,16 +412,19 @@ private static bool IsNotInstanceOfTypeFailing(object? value, [NotNullWhen(false [DoesNotReturn] private static void ReportAssertIsNotInstanceOfTypeFailed(object? value, Type? wrongType, string? userMessage, string valueExpression) { - string typeName = wrongType?.Name ?? "null"; - StructuredAssertionMessage msg = new($"Expected value to not be of type {typeName} (or derived)."); + StructuredAssertionMessage msg = wrongType is null + ? new("Cannot check type because the not-expected type argument is null.") + : new($"Expected value to not be of type {wrongType.Name} (or derived)."); msg.WithUserMessage(userMessage); if (wrongType is not null) { + string actualTypeText = value?.GetType().ToString() ?? "null"; EvidenceBlock evidence = EvidenceBlock.Create() .AddLine("not expected type:", $"{wrongType} (or derived)") - .AddLine("actual value:", AssertionValueRenderer.RenderValue(value)); - msg.WithEvidence(evidence); + .AddLine("actual type:", actualTypeText); + msg.WithEvidence(evidence) + .WithExpectedAndActual($"{wrongType} (or derived)", actualTypeText); } msg.WithCallSiteExpression(FormatCallSiteExpression("Assert.IsNotInstanceOfType", valueExpression, "")); diff --git a/src/TestFramework/TestFramework/Assertions/Assert.cs b/src/TestFramework/TestFramework/Assertions/Assert.cs index 4c7b540251..a6fe7fc1ae 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.cs @@ -188,27 +188,35 @@ internal static void ThrowAssertFailed(StructuredAssertionMessage structuredMess return null; } - string arg = expression.Contains('\n') || expression.Contains('\r') ? placeholder : expression; + string arg = IsMultiline(expression) ? placeholder : expression; return $"{assertionMethodName}({arg})"; } /// /// Formats a call-site expression for display at the bottom of a structured assertion message, /// using two captured expressions. Multiline expressions are replaced with the supplied placeholders. + /// When only one expression is empty/whitespace, its placeholder is used so the partial call site is still shown; + /// only when both expressions are empty/whitespace is the entire call-site line suppressed. /// internal static string? FormatCallSiteExpression(string assertionMethodName, string expression1, string expression2, string placeholder1 = "", string placeholder2 = "") { - if (string.IsNullOrWhiteSpace(expression1) || string.IsNullOrWhiteSpace(expression2)) + bool empty1 = string.IsNullOrWhiteSpace(expression1); + bool empty2 = string.IsNullOrWhiteSpace(expression2); + if (empty1 && empty2) { return null; } - string arg1 = expression1.Contains('\n') || expression1.Contains('\r') ? placeholder1 : expression1; - string arg2 = expression2.Contains('\n') || expression2.Contains('\r') ? placeholder2 : expression2; + string arg1 = empty1 || IsMultiline(expression1) ? placeholder1 : expression1; + string arg2 = empty2 || IsMultiline(expression2) ? placeholder2 : expression2; return $"{assertionMethodName}({arg1}, {arg2})"; } + // string.Contains(char) is not available on netstandard2.0 / net462, so use IndexOf to check for newline characters. + private static bool IsMultiline(string expression) + => expression.IndexOf('\n') >= 0 || expression.IndexOf('\r') >= 0; + private static string FormatAssertionFailed(string assertionName, string? message) { string failedMessage = string.Format(CultureInfo.CurrentCulture, FrameworkMessages.AssertionFailed, assertionName); diff --git a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.AreSame.cs b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.AreSame.cs index 58fe6db21c..de766093f7 100644 --- a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.AreSame.cs +++ b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.AreSame.cs @@ -17,7 +17,15 @@ public void AreSame_PassDifferentObject_ShouldFail() { Action action = () => Assert.AreSame(new object(), new object()); action.Should().Throw() - .WithMessage($"Assertion failed. Expected both values to refer to the same object.{Environment.NewLine}{Environment.NewLine}expected: System.Object (hash: 0x*){Environment.NewLine}actual: System.Object (hash: 0x*){Environment.NewLine}{Environment.NewLine}Assert.AreSame(new object(), new object())"); + .WithMessage( + """ + Assertion failed. Expected both values to refer to the same object. + + expected: System.Object (hash: 0x*) + actual: System.Object (hash: 0x*) + + Assert.AreSame(new object(), new object()) + """); } public void AreSame_StringMessage_PassSameObject_ShouldPass() @@ -30,7 +38,16 @@ public void AreSame_StringMessage_PassDifferentObject_ShouldFail() { Action action = () => Assert.AreSame(new object(), new object(), "User-provided message"); action.Should().Throw() - .WithMessage($"Assertion failed. Expected both values to refer to the same object.{Environment.NewLine}User-provided message{Environment.NewLine}{Environment.NewLine}expected: System.Object (hash: 0x*){Environment.NewLine}actual: System.Object (hash: 0x*){Environment.NewLine}{Environment.NewLine}Assert.AreSame(new object(), new object())"); + .WithMessage( + """ + Assertion failed. Expected both values to refer to the same object. + User-provided message + + expected: System.Object (hash: 0x*) + actual: System.Object (hash: 0x*) + + Assert.AreSame(new object(), new object()) + """); } public void AreSame_InterpolatedString_PassSameObject_ShouldPass() @@ -45,7 +62,16 @@ public async Task AreSame_InterpolatedString_PassDifferentObject_ShouldFail() DummyClassTrackingToStringCalls o = new(); DateTime dateTime = DateTime.Now; Func action = async () => Assert.AreSame(new object(), new object(), $"User-provided message. {o}, {o,35}, {await GetHelloStringAsync()}, {new DummyIFormattable()}, {dateTime:tt}, {dateTime,5:tt}"); - (await action.Should().ThrowAsync()).WithMessage($"Assertion failed. Expected both values to refer to the same object.{Environment.NewLine}User-provided message. DummyClassTrackingToStringCalls, DummyClassTrackingToStringCalls, Hello, DummyIFormattable.ToString(), {string.Format(null, "{0:tt}", dateTime)}, {string.Format(null, "{0,5:tt}", dateTime)}{Environment.NewLine}{Environment.NewLine}expected: System.Object (hash: 0x*){Environment.NewLine}actual: System.Object (hash: 0x*){Environment.NewLine}{Environment.NewLine}Assert.AreSame(new object(), new object())"); + (await action.Should().ThrowAsync()).WithMessage( + $$""" + Assertion failed. Expected both values to refer to the same object. + User-provided message. DummyClassTrackingToStringCalls, DummyClassTrackingToStringCalls, Hello, DummyIFormattable.ToString(), {{string.Format(null, "{0:tt}", dateTime)}}, {{string.Format(null, "{0,5:tt}", dateTime)}} + + expected: System.Object (hash: 0x*) + actual: System.Object (hash: 0x*) + + Assert.AreSame(new object(), new object()) + """); o.WasToStringCalled.Should().BeTrue(); } @@ -56,21 +82,41 @@ public void AreSame_BothAreValueTypes_ShouldFailWithSpecializedMessage() { Action action = () => Assert.AreSame(1, 1); action.Should().Throw() - .WithMessage($"Assertion failed. Expected both values to refer to the same object.{Environment.NewLine}Do not pass value types to AreSame \u2014 value types are boxed on each call, so references will never be the same.{Environment.NewLine}{Environment.NewLine}Assert.AreSame(1, 1)"); + .WithMessage( + """ + Assertion failed. Expected both values to refer to the same object. + Do not pass value types to AreSame — value types are boxed on each call, so references will never be the same. + + Assert.AreSame(1, 1) + """); } public void AreSame_StringMessage_BothAreValueTypes_ShouldFailWithSpecializedMessage() { Action action = () => Assert.AreSame(1, 1, "User-provided message"); action.Should().Throw() - .WithMessage($"Assertion failed. Expected both values to refer to the same object.{Environment.NewLine}Do not pass value types to AreSame \u2014 value types are boxed on each call, so references will never be the same.{Environment.NewLine}User-provided message{Environment.NewLine}{Environment.NewLine}Assert.AreSame(1, 1)"); + .WithMessage( + """ + Assertion failed. Expected both values to refer to the same object. + Do not pass value types to AreSame — value types are boxed on each call, so references will never be the same. + User-provided message + + Assert.AreSame(1, 1) + """); } public void AreSame_InterpolatedString_BothAreValueTypes_ShouldFailWithSpecializedMessage() { Action action = () => Assert.AreSame(1, 1, $"User-provided message {new object().GetType()}"); action.Should().Throw() - .WithMessage($"Assertion failed. Expected both values to refer to the same object.{Environment.NewLine}Do not pass value types to AreSame \u2014 value types are boxed on each call, so references will never be the same.{Environment.NewLine}User-provided message System.Object{Environment.NewLine}{Environment.NewLine}Assert.AreSame(1, 1)"); + .WithMessage( + """ + Assertion failed. Expected both values to refer to the same object. + Do not pass value types to AreSame — value types are boxed on each call, so references will never be the same. + User-provided message System.Object + + Assert.AreSame(1, 1) + """); } public void AreSame_ExpectedNull_ShouldFailWithNullMessage() @@ -78,7 +124,15 @@ public void AreSame_ExpectedNull_ShouldFailWithNullMessage() object? expected = null; Action action = () => Assert.AreSame(expected, new object()); action.Should().Throw() - .WithMessage($"Assertion failed. Expected both values to refer to the same object.{Environment.NewLine}{Environment.NewLine}expected: null{Environment.NewLine}actual: System.Object (hash: 0x*){Environment.NewLine}{Environment.NewLine}Assert.AreSame(expected, new object())"); + .WithMessage( + """ + Assertion failed. Expected both values to refer to the same object. + + expected: null + actual: System.Object (hash: 0x*) + + Assert.AreSame(expected, new object()) + """); } public void AreSame_ActualNull_ShouldFailWithNullMessage() @@ -86,7 +140,15 @@ public void AreSame_ActualNull_ShouldFailWithNullMessage() object? actual = null; Action action = () => Assert.AreSame(new object(), actual); action.Should().Throw() - .WithMessage($"Assertion failed. Expected both values to refer to the same object.{Environment.NewLine}{Environment.NewLine}expected: System.Object (hash: 0x*){Environment.NewLine}actual: null{Environment.NewLine}{Environment.NewLine}Assert.AreSame(new object(), actual)"); + .WithMessage( + """ + Assertion failed. Expected both values to refer to the same object. + + expected: System.Object (hash: 0x*) + actual: null + + Assert.AreSame(new object(), actual) + """); } public void AreSame_StringMessage_ExpectedNull_ShouldFailWithNullMessage() @@ -94,7 +156,16 @@ public void AreSame_StringMessage_ExpectedNull_ShouldFailWithNullMessage() object? expected = null; Action action = () => Assert.AreSame(expected, new object(), "User-provided message"); action.Should().Throw() - .WithMessage($"Assertion failed. Expected both values to refer to the same object.{Environment.NewLine}User-provided message{Environment.NewLine}{Environment.NewLine}expected: null{Environment.NewLine}actual: System.Object (hash: 0x*){Environment.NewLine}{Environment.NewLine}Assert.AreSame(expected, new object())"); + .WithMessage( + """ + Assertion failed. Expected both values to refer to the same object. + User-provided message + + expected: null + actual: System.Object (hash: 0x*) + + Assert.AreSame(expected, new object()) + """); } public void AreSame_StringMessage_ActualNull_ShouldFailWithNullMessage() @@ -102,7 +173,16 @@ public void AreSame_StringMessage_ActualNull_ShouldFailWithNullMessage() object? actual = null; Action action = () => Assert.AreSame(new object(), actual, "User-provided message"); action.Should().Throw() - .WithMessage($"Assertion failed. Expected both values to refer to the same object.{Environment.NewLine}User-provided message{Environment.NewLine}{Environment.NewLine}expected: System.Object (hash: 0x*){Environment.NewLine}actual: null{Environment.NewLine}{Environment.NewLine}Assert.AreSame(new object(), actual)"); + .WithMessage( + """ + Assertion failed. Expected both values to refer to the same object. + User-provided message + + expected: System.Object (hash: 0x*) + actual: null + + Assert.AreSame(new object(), actual) + """); } public void AreSame_InterpolatedString_ExpectedNull_ShouldFailWithNullMessage() @@ -110,7 +190,16 @@ public void AreSame_InterpolatedString_ExpectedNull_ShouldFailWithNullMessage() object? expected = null; Action action = () => Assert.AreSame(expected, new object(), $"User-provided message {new object().GetType()}"); action.Should().Throw() - .WithMessage($"Assertion failed. Expected both values to refer to the same object.{Environment.NewLine}User-provided message System.Object{Environment.NewLine}{Environment.NewLine}expected: null{Environment.NewLine}actual: System.Object (hash: 0x*){Environment.NewLine}{Environment.NewLine}Assert.AreSame(expected, new object())"); + .WithMessage( + """ + Assertion failed. Expected both values to refer to the same object. + User-provided message System.Object + + expected: null + actual: System.Object (hash: 0x*) + + Assert.AreSame(expected, new object()) + """); } public void AreSame_InterpolatedString_ActualNull_ShouldFailWithNullMessage() @@ -118,7 +207,29 @@ public void AreSame_InterpolatedString_ActualNull_ShouldFailWithNullMessage() object? actual = null; Action action = () => Assert.AreSame(new object(), actual, $"User-provided message {new object().GetType()}"); action.Should().Throw() - .WithMessage($"Assertion failed. Expected both values to refer to the same object.{Environment.NewLine}User-provided message System.Object{Environment.NewLine}{Environment.NewLine}expected: System.Object (hash: 0x*){Environment.NewLine}actual: null{Environment.NewLine}{Environment.NewLine}Assert.AreSame(new object(), actual)"); + .WithMessage( + """ + Assertion failed. Expected both values to refer to the same object. + User-provided message System.Object + + expected: System.Object (hash: 0x*) + actual: null + + Assert.AreSame(new object(), actual) + """); + } + + public void AreSame_ShouldPopulateExpectedAndActualPayloadOnFailure() + { + object expected = new(); + object actual = new(); + Action action = () => Assert.AreSame(expected, actual); + + AssertFailedException ex = action.Should().Throw().Which; + ex.ExpectedText.Should().Be($"System.Object (hash: 0x{System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(expected):X})"); + ex.ActualText.Should().Be($"System.Object (hash: 0x{System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(actual):X})"); + ex.Data["assert.expected"].Should().Be(ex.ExpectedText); + ex.Data["assert.actual"].Should().Be(ex.ActualText); } public void AreNotSame_PassSameObject_ShouldFail() @@ -126,7 +237,13 @@ public void AreNotSame_PassSameObject_ShouldFail() object o = new(); Action action = () => Assert.AreNotSame(o, o); action.Should().Throw() - .WithMessage($"Assertion failed. Expected values to refer to different objects.{Environment.NewLine}Both variables refer to the same object.{Environment.NewLine}{Environment.NewLine}Assert.AreNotSame(o, o)"); + .WithMessage( + """ + Assertion failed. Expected values to refer to different objects. + Both values refer to the same object. + + Assert.AreNotSame(o, o) + """); } public void AreNotSame_StringMessage_PassDifferentObject_ShouldPass() @@ -137,7 +254,14 @@ public void AreNotSame_StringMessage_PassSameObject_ShouldFail() object o = new(); Action action = () => Assert.AreNotSame(o, o, "User-provided message"); action.Should().Throw() - .WithMessage($"Assertion failed. Expected values to refer to different objects.{Environment.NewLine}Both variables refer to the same object.{Environment.NewLine}User-provided message{Environment.NewLine}{Environment.NewLine}Assert.AreNotSame(o, o)"); + .WithMessage( + """ + Assertion failed. Expected values to refer to different objects. + Both values refer to the same object. + User-provided message + + Assert.AreNotSame(o, o) + """); } public void AreNotSame_InterpolatedString_PassDifferentObject_ShouldPass() @@ -152,7 +276,14 @@ public async Task AreNotSame_InterpolatedString_PassSameObject_ShouldFail() DummyClassTrackingToStringCalls o = new(); DateTime dateTime = DateTime.Now; Func action = async () => Assert.AreNotSame(o, o, $"User-provided message. {o}, {o,35}, {await GetHelloStringAsync()}, {new DummyIFormattable()}, {dateTime:tt}, {dateTime,5:tt}"); - (await action.Should().ThrowAsync()).WithMessage($"Assertion failed. Expected values to refer to different objects.{Environment.NewLine}Both variables refer to the same object.{Environment.NewLine}User-provided message. DummyClassTrackingToStringCalls, DummyClassTrackingToStringCalls, Hello, DummyIFormattable.ToString(), {string.Format(null, "{0:tt}", dateTime)}, {string.Format(null, "{0,5:tt}", dateTime)}{Environment.NewLine}{Environment.NewLine}Assert.AreNotSame(o, o)"); + (await action.Should().ThrowAsync()).WithMessage( + $$""" + Assertion failed. Expected values to refer to different objects. + Both values refer to the same object. + User-provided message. DummyClassTrackingToStringCalls, DummyClassTrackingToStringCalls, Hello, DummyIFormattable.ToString(), {{string.Format(null, "{0:tt}", dateTime)}}, {{string.Format(null, "{0,5:tt}", dateTime)}} + + Assert.AreNotSame(o, o) + """); o.WasToStringCalled.Should().BeTrue(); } @@ -162,7 +293,13 @@ public void AreNotSame_BothNull_ShouldFailWithNullMessage() object? actual = null; Action action = () => Assert.AreNotSame(notExpected, actual); action.Should().Throw() - .WithMessage($"Assertion failed. Expected values to refer to different objects.{Environment.NewLine}Both values are null.{Environment.NewLine}{Environment.NewLine}Assert.AreNotSame(notExpected, actual)"); + .WithMessage( + """ + Assertion failed. Expected values to refer to different objects. + Both values are null. + + Assert.AreNotSame(notExpected, actual) + """); } public void AreNotSame_StringMessage_BothNull_ShouldFailWithNullMessage() @@ -171,7 +308,14 @@ public void AreNotSame_StringMessage_BothNull_ShouldFailWithNullMessage() object? actual = null; Action action = () => Assert.AreNotSame(notExpected, actual, "User-provided message"); action.Should().Throw() - .WithMessage($"Assertion failed. Expected values to refer to different objects.{Environment.NewLine}Both values are null.{Environment.NewLine}User-provided message{Environment.NewLine}{Environment.NewLine}Assert.AreNotSame(notExpected, actual)"); + .WithMessage( + """ + Assertion failed. Expected values to refer to different objects. + Both values are null. + User-provided message + + Assert.AreNotSame(notExpected, actual) + """); } public void AreNotSame_InterpolatedString_BothNull_ShouldFailWithNullMessage() @@ -180,6 +324,13 @@ public void AreNotSame_InterpolatedString_BothNull_ShouldFailWithNullMessage() object? actual = null; Action action = () => Assert.AreNotSame(notExpected, actual, $"User-provided message {new object().GetType()}"); action.Should().Throw() - .WithMessage($"Assertion failed. Expected values to refer to different objects.{Environment.NewLine}Both values are null.{Environment.NewLine}User-provided message System.Object{Environment.NewLine}{Environment.NewLine}Assert.AreNotSame(notExpected, actual)"); + .WithMessage( + """ + Assertion failed. Expected values to refer to different objects. + Both values are null. + User-provided message System.Object + + Assert.AreNotSame(notExpected, actual) + """); } } diff --git a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.FormatCallSiteExpressionTests.cs b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.FormatCallSiteExpressionTests.cs new file mode 100644 index 0000000000..7351bd86e9 --- /dev/null +++ b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.FormatCallSiteExpressionTests.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using AwesomeAssertions; + +namespace Microsoft.VisualStudio.TestPlatform.TestFramework.UnitTests; + +public partial class AssertTests +{ + public void FormatCallSiteExpression_SingleArg_NormalExpression_ReturnsExpression() + => Assert.FormatCallSiteExpression("Assert.X", "value").Should().Be("Assert.X(value)"); + + public void FormatCallSiteExpression_SingleArg_NullOrWhitespaceExpression_ReturnsNull() + { + Assert.FormatCallSiteExpression("Assert.X", string.Empty).Should().BeNull(); + Assert.FormatCallSiteExpression("Assert.X", " ").Should().BeNull(); + } + + public void FormatCallSiteExpression_SingleArg_MultilineExpression_UsesPlaceholder() + { + Assert.FormatCallSiteExpression("Assert.X", "foo\nbar").Should().Be("Assert.X()"); + Assert.FormatCallSiteExpression("Assert.X", "foo\r\nbar").Should().Be("Assert.X()"); + Assert.FormatCallSiteExpression("Assert.X", "foo\rbar").Should().Be("Assert.X()"); + } + + public void FormatCallSiteExpression_TwoArgs_NormalExpressions_ReturnsExpressions() + => Assert.FormatCallSiteExpression("Assert.X", expression1: "a", expression2: "b").Should().Be("Assert.X(a, b)"); + + public void FormatCallSiteExpression_TwoArgs_BothEmpty_ReturnsNull() + => Assert.FormatCallSiteExpression("Assert.X", expression1: string.Empty, expression2: " ").Should().BeNull(); + + public void FormatCallSiteExpression_TwoArgs_FirstEmpty_UsesPlaceholderForFirst() + => Assert.FormatCallSiteExpression("Assert.X", expression1: string.Empty, expression2: "b").Should().Be("Assert.X(, b)"); + + public void FormatCallSiteExpression_TwoArgs_SecondEmpty_UsesPlaceholderForSecond() + => Assert.FormatCallSiteExpression("Assert.X", expression1: "a", expression2: string.Empty).Should().Be("Assert.X(a, )"); + + public void FormatCallSiteExpression_TwoArgs_FirstMultiline_UsesPlaceholderForFirst() + => Assert.FormatCallSiteExpression("Assert.X", expression1: "a\nb", expression2: "c").Should().Be("Assert.X(, c)"); + + public void FormatCallSiteExpression_TwoArgs_SecondMultiline_UsesPlaceholderForSecond() + => Assert.FormatCallSiteExpression("Assert.X", expression1: "a", expression2: "b\nc").Should().Be("Assert.X(a, )"); + + public void FormatCallSiteExpression_TwoArgs_BothMultiline_UsesBothPlaceholders() + => Assert.FormatCallSiteExpression("Assert.X", expression1: "a\nb", expression2: "c\nd").Should().Be("Assert.X(, )"); +} diff --git a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.IsExactInstanceOfTypeTests.cs b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.IsExactInstanceOfTypeTests.cs index e14f3af07c..e6469596b8 100644 --- a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.IsExactInstanceOfTypeTests.cs +++ b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.IsExactInstanceOfTypeTests.cs @@ -12,21 +12,42 @@ public void ExactInstanceOfTypeShouldFailWhenValueIsNull() { Action action = () => Assert.IsExactInstanceOfType(null, typeof(AssertTests)); action.Should().Throw() - .WithMessage($"Assertion failed. Expected value to be exactly of type AssertTests.{Environment.NewLine}{Environment.NewLine}expected type: Microsoft.VisualStudio.TestPlatform.TestFramework.UnitTests.AssertTests{Environment.NewLine}actual: null{Environment.NewLine}{Environment.NewLine}Assert.IsExactInstanceOfType(null)"); + .WithMessage( + """ + Assertion failed. Expected value to be exactly of type AssertTests. + + expected type: Microsoft.VisualStudio.TestPlatform.TestFramework.UnitTests.AssertTests + actual: null + + Assert.IsExactInstanceOfType(null) + """); } public void ExactInstanceOfTypeShouldFailWhenTypeIsNull() { Action action = () => Assert.IsExactInstanceOfType(5, null); action.Should().Throw() - .WithMessage($"Assertion failed. Expected value to be exactly of type null.{Environment.NewLine}{Environment.NewLine}Assert.IsExactInstanceOfType(5)"); + .WithMessage( + """ + Assertion failed. Cannot check type because the expected type argument is null. + + Assert.IsExactInstanceOfType(5) + """); } public void ExactInstanceOfTypeShouldFailWhenTypeIsMismatched() { Action action = () => Assert.IsExactInstanceOfType(5, typeof(string)); action.Should().Throw() - .WithMessage($"Assertion failed. Expected value to be exactly of type String.{Environment.NewLine}{Environment.NewLine}expected type: System.String{Environment.NewLine}actual type: System.Int32{Environment.NewLine}actual value: 5{Environment.NewLine}{Environment.NewLine}Assert.IsExactInstanceOfType(5)"); + .WithMessage( + """ + Assertion failed. Expected value to be exactly of type String. + + expected type: System.String + actual type: System.Int32 + + Assert.IsExactInstanceOfType(5) + """); } public void ExactInstanceOfTypeShouldPassOnSameInstance() => Assert.IsExactInstanceOfType(5, typeof(int)); @@ -35,7 +56,15 @@ public void ExactInstanceOfTypeShouldFailOnHigherInstance() { Action action = () => Assert.IsExactInstanceOfType(5, typeof(object)); action.Should().Throw() - .WithMessage($"Assertion failed. Expected value to be exactly of type Object.{Environment.NewLine}{Environment.NewLine}expected type: System.Object{Environment.NewLine}actual type: System.Int32{Environment.NewLine}actual value: 5{Environment.NewLine}{Environment.NewLine}Assert.IsExactInstanceOfType(5)"); + .WithMessage( + """ + Assertion failed. Expected value to be exactly of type Object. + + expected type: System.Object + actual type: System.Int32 + + Assert.IsExactInstanceOfType(5) + """); } public void ExactInstanceOfTypeShouldFailOnDerivedType() @@ -43,7 +72,15 @@ public void ExactInstanceOfTypeShouldFailOnDerivedType() object x = new MemoryStream(); Action action = () => Assert.IsExactInstanceOfType(x, typeof(Stream)); action.Should().Throw() - .WithMessage($"Assertion failed. Expected value to be exactly of type Stream.{Environment.NewLine}{Environment.NewLine}expected type: System.IO.Stream{Environment.NewLine}actual type: System.IO.MemoryStream{Environment.NewLine}actual value: *{Environment.NewLine}{Environment.NewLine}Assert.IsExactInstanceOfType(x)"); + .WithMessage( + """ + Assertion failed. Expected value to be exactly of type Stream. + + expected type: System.IO.Stream + actual type: System.IO.MemoryStream + + Assert.IsExactInstanceOfType(x) + """); } public void ExactInstanceOfTypeShouldPassOnExactType() @@ -56,21 +93,45 @@ public void ExactInstanceOfType_WithStringMessage_ShouldFailWhenValueIsNull() { Action action = () => Assert.IsExactInstanceOfType(null, typeof(AssertTests), "User-provided message"); action.Should().Throw() - .WithMessage($"Assertion failed. Expected value to be exactly of type AssertTests.{Environment.NewLine}User-provided message{Environment.NewLine}{Environment.NewLine}expected type: Microsoft.VisualStudio.TestPlatform.TestFramework.UnitTests.AssertTests{Environment.NewLine}actual: null{Environment.NewLine}{Environment.NewLine}Assert.IsExactInstanceOfType(null)"); + .WithMessage( + """ + Assertion failed. Expected value to be exactly of type AssertTests. + User-provided message + + expected type: Microsoft.VisualStudio.TestPlatform.TestFramework.UnitTests.AssertTests + actual: null + + Assert.IsExactInstanceOfType(null) + """); } public void ExactInstanceOfType_WithStringMessage_ShouldFailWhenTypeIsNull() { Action action = () => Assert.IsExactInstanceOfType(5, null, "User-provided message"); action.Should().Throw() - .WithMessage($"Assertion failed. Expected value to be exactly of type null.{Environment.NewLine}User-provided message{Environment.NewLine}{Environment.NewLine}Assert.IsExactInstanceOfType(5)"); + .WithMessage( + """ + Assertion failed. Cannot check type because the expected type argument is null. + User-provided message + + Assert.IsExactInstanceOfType(5) + """); } public void ExactInstanceOfType_WithStringMessage_ShouldFailWhenTypeIsMismatched() { Action action = () => Assert.IsExactInstanceOfType(5, typeof(string), "User-provided message"); action.Should().Throw() - .WithMessage($"Assertion failed. Expected value to be exactly of type String.{Environment.NewLine}User-provided message{Environment.NewLine}{Environment.NewLine}expected type: System.String{Environment.NewLine}actual type: System.Int32{Environment.NewLine}actual value: 5{Environment.NewLine}{Environment.NewLine}Assert.IsExactInstanceOfType(5)"); + .WithMessage( + """ + Assertion failed. Expected value to be exactly of type String. + User-provided message + + expected type: System.String + actual type: System.Int32 + + Assert.IsExactInstanceOfType(5) + """); } public void ExactInstanceOfType_WithStringMessage_ShouldPassWhenTypeIsCorrect() @@ -82,7 +143,16 @@ public async Task ExactInstanceOfType_WithInterpolatedString_ShouldFailWhenValue DateTime dateTime = DateTime.Now; Func action = async () => Assert.IsExactInstanceOfType(null, typeof(AssertTests), $"User-provided message. {o}, {o,35}, {await GetHelloStringAsync()}, {new DummyIFormattable()}, {dateTime:tt}, {dateTime,5:tt}"); (await action.Should().ThrowAsync()) - .WithMessage($"Assertion failed. Expected value to be exactly of type AssertTests.{Environment.NewLine}User-provided message. DummyClassTrackingToStringCalls, DummyClassTrackingToStringCalls, Hello, DummyIFormattable.ToString(), {string.Format(null, "{0:tt}", dateTime)}, {string.Format(null, "{0,5:tt}", dateTime)}{Environment.NewLine}{Environment.NewLine}expected type: Microsoft.VisualStudio.TestPlatform.TestFramework.UnitTests.AssertTests{Environment.NewLine}actual: null{Environment.NewLine}{Environment.NewLine}Assert.IsExactInstanceOfType(null)"); + .WithMessage( + $$""" + Assertion failed. Expected value to be exactly of type AssertTests. + User-provided message. DummyClassTrackingToStringCalls, DummyClassTrackingToStringCalls, Hello, DummyIFormattable.ToString(), {{string.Format(null, "{0:tt}", dateTime)}}, {{string.Format(null, "{0,5:tt}", dateTime)}} + + expected type: Microsoft.VisualStudio.TestPlatform.TestFramework.UnitTests.AssertTests + actual: null + + Assert.IsExactInstanceOfType(null) + """); o.WasToStringCalled.Should().BeTrue(); } @@ -91,7 +161,13 @@ public void ExactInstanceOfType_WithInterpolatedString_ShouldFailWhenTypeIsNull( DummyClassTrackingToStringCalls o = new(); Action action = () => Assert.IsExactInstanceOfType(5, null, $"User-provided message {o}"); action.Should().Throw() - .WithMessage($"Assertion failed. Expected value to be exactly of type null.{Environment.NewLine}User-provided message DummyClassTrackingToStringCalls{Environment.NewLine}{Environment.NewLine}Assert.IsExactInstanceOfType(5)"); + .WithMessage( + """ + Assertion failed. Cannot check type because the expected type argument is null. + User-provided message DummyClassTrackingToStringCalls + + Assert.IsExactInstanceOfType(5) + """); o.WasToStringCalled.Should().BeTrue(); } @@ -100,7 +176,16 @@ public void ExactInstanceOfType_WithInterpolatedString_ShouldFailWhenTypeIsMisma DummyClassTrackingToStringCalls o = new(); Action action = () => Assert.IsExactInstanceOfType(5, typeof(string), $"User-provided message {o}"); action.Should().Throw() - .WithMessage($"Assertion failed. Expected value to be exactly of type String.{Environment.NewLine}User-provided message DummyClassTrackingToStringCalls{Environment.NewLine}{Environment.NewLine}expected type: System.String{Environment.NewLine}actual type: System.Int32{Environment.NewLine}actual value: 5{Environment.NewLine}{Environment.NewLine}Assert.IsExactInstanceOfType(5)"); + .WithMessage( + """ + Assertion failed. Expected value to be exactly of type String. + User-provided message DummyClassTrackingToStringCalls + + expected type: System.String + actual type: System.Int32 + + Assert.IsExactInstanceOfType(5) + """); o.WasToStringCalled.Should().BeTrue(); } @@ -134,21 +219,45 @@ public void ExactInstanceNotOfTypeShouldFailOnExactType() object x = new MemoryStream(); Action action = () => Assert.IsNotExactInstanceOfType(x, typeof(MemoryStream)); action.Should().Throw() - .WithMessage($"Assertion failed. Expected value to not be exactly of type MemoryStream.{Environment.NewLine}{Environment.NewLine}not expected type: System.IO.MemoryStream{Environment.NewLine}actual value: *{Environment.NewLine}{Environment.NewLine}Assert.IsNotExactInstanceOfType(x)"); + .WithMessage( + """ + Assertion failed. Expected value to not be exactly of type MemoryStream. + + not expected type: System.IO.MemoryStream + actual type: System.IO.MemoryStream + + Assert.IsNotExactInstanceOfType(x) + """); } public void IsExactInstanceOfTypeUsingGenericType_WhenValueIsNull_Fails() { Action action = () => Assert.IsExactInstanceOfType(null); action.Should().Throw() - .WithMessage($"Assertion failed. Expected value to be exactly of type AssertTests.{Environment.NewLine}{Environment.NewLine}expected type: Microsoft.VisualStudio.TestPlatform.TestFramework.UnitTests.AssertTests{Environment.NewLine}actual: null{Environment.NewLine}{Environment.NewLine}Assert.IsExactInstanceOfType(null)"); + .WithMessage( + """ + Assertion failed. Expected value to be exactly of type AssertTests. + + expected type: Microsoft.VisualStudio.TestPlatform.TestFramework.UnitTests.AssertTests + actual: null + + Assert.IsExactInstanceOfType(null) + """); } public void IsExactInstanceOfTypeUsingGenericType_WhenTypeMismatch_Fails() { Action action = () => Assert.IsExactInstanceOfType(5); action.Should().Throw() - .WithMessage($"Assertion failed. Expected value to be exactly of type String.{Environment.NewLine}{Environment.NewLine}expected type: System.String{Environment.NewLine}actual type: System.Int32{Environment.NewLine}actual value: 5{Environment.NewLine}{Environment.NewLine}Assert.IsExactInstanceOfType(5)"); + .WithMessage( + """ + Assertion failed. Expected value to be exactly of type String. + + expected type: System.String + actual type: System.Int32 + + Assert.IsExactInstanceOfType(5) + """); } public void IsExactInstanceOfTypeUsingGenericType_WhenDerivedType_Fails() @@ -156,7 +265,15 @@ public void IsExactInstanceOfTypeUsingGenericType_WhenDerivedType_Fails() object x = new MemoryStream(); Action action = () => Assert.IsExactInstanceOfType(x); action.Should().Throw() - .WithMessage($"Assertion failed. Expected value to be exactly of type Stream.{Environment.NewLine}{Environment.NewLine}expected type: System.IO.Stream{Environment.NewLine}actual type: System.IO.MemoryStream{Environment.NewLine}actual value: *{Environment.NewLine}{Environment.NewLine}Assert.IsExactInstanceOfType(x)"); + .WithMessage( + """ + Assertion failed. Expected value to be exactly of type Stream. + + expected type: System.IO.Stream + actual type: System.IO.MemoryStream + + Assert.IsExactInstanceOfType(x) + """); } public void IsExactInstanceOfTypeUsingGenericType_OnSameInstance_DoesNotThrow() => Assert.IsExactInstanceOfType(5); @@ -178,7 +295,15 @@ public void IsExactInstanceOfTypeUsingGenericType_OnHigherInstance_Fails() { Action action = () => Assert.IsExactInstanceOfType(5); action.Should().Throw() - .WithMessage($"Assertion failed. Expected value to be exactly of type Object.{Environment.NewLine}{Environment.NewLine}expected type: System.Object{Environment.NewLine}actual type: System.Int32{Environment.NewLine}actual value: 5{Environment.NewLine}{Environment.NewLine}Assert.IsExactInstanceOfType(5)"); + .WithMessage( + """ + Assertion failed. Expected value to be exactly of type Object. + + expected type: System.Object + actual type: System.Int32 + + Assert.IsExactInstanceOfType(5) + """); } public void IsExactInstanceOfTypeUsingGenericTypeWithReturn_OnExactType_DoesNotThrow() @@ -205,7 +330,15 @@ public void IsNotExactInstanceOfTypeUsingGenericType_OnExactType_Fails() object x = new MemoryStream(); Action action = () => Assert.IsNotExactInstanceOfType(x); action.Should().Throw() - .WithMessage($"Assertion failed. Expected value to not be exactly of type MemoryStream.{Environment.NewLine}{Environment.NewLine}not expected type: System.IO.MemoryStream{Environment.NewLine}actual value: *{Environment.NewLine}{Environment.NewLine}Assert.IsNotExactInstanceOfType(x)"); + .WithMessage( + """ + Assertion failed. Expected value to not be exactly of type MemoryStream. + + not expected type: System.IO.MemoryStream + actual type: System.IO.MemoryStream + + Assert.IsNotExactInstanceOfType(x) + """); } public void IsExactInstanceOfType_WhenNonNullNullableValue_LearnNonNull() @@ -277,4 +410,41 @@ public void IsNotExactInstanceOfType_WhenNonNullNullableTypeAndMessage_LearnNonN Assert.IsNotExactInstanceOfType(new object(), intType, "my message"); _ = intType.ToString(); // no warning about possible null } + + public void IsExactInstanceOfType_OnFailure_ShouldPopulateExpectedAndActualPayload() + { + try + { + Assert.IsExactInstanceOfType(5, typeof(string)); + } + catch (AssertFailedException ex) + { + ex.ExpectedText.Should().Be("System.String"); + ex.ActualText.Should().Be("System.Int32"); + ex.Data["assert.expected"].Should().Be("System.String"); + ex.Data["assert.actual"].Should().Be("System.Int32"); + return; + } + + throw new AssertFailedException("Expected AssertFailedException was not thrown."); + } + + public void IsNotExactInstanceOfType_OnFailure_ShouldPopulateExpectedAndActualPayload() + { + object x = new MemoryStream(); + try + { + Assert.IsNotExactInstanceOfType(x, typeof(MemoryStream)); + } + catch (AssertFailedException ex) + { + ex.ExpectedText.Should().Be("System.IO.MemoryStream"); + ex.ActualText.Should().Be("System.IO.MemoryStream"); + ex.Data["assert.expected"].Should().Be("System.IO.MemoryStream"); + ex.Data["assert.actual"].Should().Be("System.IO.MemoryStream"); + return; + } + + throw new AssertFailedException("Expected AssertFailedException was not thrown."); + } } diff --git a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.IsInstanceOfTypeTests.cs b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.IsInstanceOfTypeTests.cs index 9ddd682da6..210d3edb14 100644 --- a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.IsInstanceOfTypeTests.cs +++ b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.IsInstanceOfTypeTests.cs @@ -12,21 +12,42 @@ public void InstanceOfTypeShouldFailWhenValueIsNull() { Action action = () => Assert.IsInstanceOfType(null, typeof(AssertTests)); action.Should().Throw() - .WithMessage($"Assertion failed. Expected value to be of type AssertTests (or derived).{Environment.NewLine}{Environment.NewLine}expected type: Microsoft.VisualStudio.TestPlatform.TestFramework.UnitTests.AssertTests (or derived){Environment.NewLine}actual: null{Environment.NewLine}{Environment.NewLine}Assert.IsInstanceOfType(null)"); + .WithMessage( + """ + Assertion failed. Expected value to be of type AssertTests (or derived). + + expected type: Microsoft.VisualStudio.TestPlatform.TestFramework.UnitTests.AssertTests (or derived) + actual: null + + Assert.IsInstanceOfType(null) + """); } public void InstanceOfTypeShouldFailWhenTypeIsNull() { Action action = () => Assert.IsInstanceOfType(5, null); action.Should().Throw() - .WithMessage($"Assertion failed. Expected value to be of type null (or derived).{Environment.NewLine}{Environment.NewLine}Assert.IsInstanceOfType(5)"); + .WithMessage( + """ + Assertion failed. Cannot check type because the expected type argument is null. + + Assert.IsInstanceOfType(5) + """); } public void InstanceOfTypeShouldFailWhenTypeIsMismatched() { Action action = () => Assert.IsInstanceOfType(5, typeof(string)); action.Should().Throw() - .WithMessage($"Assertion failed. Expected value to be of type String (or derived).{Environment.NewLine}{Environment.NewLine}expected type: System.String (or derived){Environment.NewLine}actual type: System.Int32{Environment.NewLine}actual value: 5{Environment.NewLine}{Environment.NewLine}Assert.IsInstanceOfType(5)"); + .WithMessage( + """ + Assertion failed. Expected value to be of type String (or derived). + + expected type: System.String (or derived) + actual type: System.Int32 + + Assert.IsInstanceOfType(5) + """); } public void InstanceOfTypeShouldPassOnSameInstance() => Assert.IsInstanceOfType(5, typeof(int)); @@ -37,21 +58,45 @@ public void InstanceOfType_WithStringMessage_ShouldFailWhenValueIsNull() { Action action = () => Assert.IsInstanceOfType(null, typeof(AssertTests), "User-provided message"); action.Should().Throw() - .WithMessage($"Assertion failed. Expected value to be of type AssertTests (or derived).{Environment.NewLine}User-provided message{Environment.NewLine}{Environment.NewLine}expected type: Microsoft.VisualStudio.TestPlatform.TestFramework.UnitTests.AssertTests (or derived){Environment.NewLine}actual: null{Environment.NewLine}{Environment.NewLine}Assert.IsInstanceOfType(null)"); + .WithMessage( + """ + Assertion failed. Expected value to be of type AssertTests (or derived). + User-provided message + + expected type: Microsoft.VisualStudio.TestPlatform.TestFramework.UnitTests.AssertTests (or derived) + actual: null + + Assert.IsInstanceOfType(null) + """); } public void InstanceOfType_WithStringMessage_ShouldFailWhenTypeIsNull() { Action action = () => Assert.IsInstanceOfType(5, null, "User-provided message"); action.Should().Throw() - .WithMessage($"Assertion failed. Expected value to be of type null (or derived).{Environment.NewLine}User-provided message{Environment.NewLine}{Environment.NewLine}Assert.IsInstanceOfType(5)"); + .WithMessage( + """ + Assertion failed. Cannot check type because the expected type argument is null. + User-provided message + + Assert.IsInstanceOfType(5) + """); } public void InstanceOfType_WithStringMessage_ShouldFailWhenTypeIsMismatched() { Action action = () => Assert.IsInstanceOfType(5, typeof(string), "User-provided message"); action.Should().Throw() - .WithMessage($"Assertion failed. Expected value to be of type String (or derived).{Environment.NewLine}User-provided message{Environment.NewLine}{Environment.NewLine}expected type: System.String (or derived){Environment.NewLine}actual type: System.Int32{Environment.NewLine}actual value: 5{Environment.NewLine}{Environment.NewLine}Assert.IsInstanceOfType(5)"); + .WithMessage( + """ + Assertion failed. Expected value to be of type String (or derived). + User-provided message + + expected type: System.String (or derived) + actual type: System.Int32 + + Assert.IsInstanceOfType(5) + """); } public void InstanceOfType_WithStringMessage_ShouldPassWhenTypeIsCorrect() @@ -63,7 +108,16 @@ public async Task InstanceOfType_WithInterpolatedString_ShouldFailWhenValueIsNul DateTime dateTime = DateTime.Now; Func action = async () => Assert.IsInstanceOfType(null, typeof(AssertTests), $"User-provided message. {o}, {o,35}, {await GetHelloStringAsync()}, {new DummyIFormattable()}, {dateTime:tt}, {dateTime,5:tt}"); (await action.Should().ThrowAsync()) - .WithMessage($"Assertion failed. Expected value to be of type AssertTests (or derived).{Environment.NewLine}User-provided message. DummyClassTrackingToStringCalls, DummyClassTrackingToStringCalls, Hello, DummyIFormattable.ToString(), {string.Format(null, "{0:tt}", dateTime)}, {string.Format(null, "{0,5:tt}", dateTime)}{Environment.NewLine}{Environment.NewLine}expected type: Microsoft.VisualStudio.TestPlatform.TestFramework.UnitTests.AssertTests (or derived){Environment.NewLine}actual: null{Environment.NewLine}{Environment.NewLine}Assert.IsInstanceOfType(null)"); + .WithMessage( + $$""" + Assertion failed. Expected value to be of type AssertTests (or derived). + User-provided message. DummyClassTrackingToStringCalls, DummyClassTrackingToStringCalls, Hello, DummyIFormattable.ToString(), {{string.Format(null, "{0:tt}", dateTime)}}, {{string.Format(null, "{0,5:tt}", dateTime)}} + + expected type: Microsoft.VisualStudio.TestPlatform.TestFramework.UnitTests.AssertTests (or derived) + actual: null + + Assert.IsInstanceOfType(null) + """); o.WasToStringCalled.Should().BeTrue(); } @@ -72,7 +126,13 @@ public void InstanceOfType_WithInterpolatedString_ShouldFailWhenTypeIsNull() DummyClassTrackingToStringCalls o = new(); Action action = () => Assert.IsInstanceOfType(5, null, $"User-provided message {o}"); action.Should().Throw() - .WithMessage($"Assertion failed. Expected value to be of type null (or derived).{Environment.NewLine}User-provided message DummyClassTrackingToStringCalls{Environment.NewLine}{Environment.NewLine}Assert.IsInstanceOfType(5)"); + .WithMessage( + """ + Assertion failed. Cannot check type because the expected type argument is null. + User-provided message DummyClassTrackingToStringCalls + + Assert.IsInstanceOfType(5) + """); o.WasToStringCalled.Should().BeTrue(); } @@ -81,7 +141,16 @@ public void InstanceOfType_WithInterpolatedString_ShouldFailWhenTypeIsMismatched DummyClassTrackingToStringCalls o = new(); Action action = () => Assert.IsInstanceOfType(5, typeof(string), $"User-provided message {o}"); action.Should().Throw() - .WithMessage($"Assertion failed. Expected value to be of type String (or derived).{Environment.NewLine}User-provided message DummyClassTrackingToStringCalls{Environment.NewLine}{Environment.NewLine}expected type: System.String (or derived){Environment.NewLine}actual type: System.Int32{Environment.NewLine}actual value: 5{Environment.NewLine}{Environment.NewLine}Assert.IsInstanceOfType(5)"); + .WithMessage( + """ + Assertion failed. Expected value to be of type String (or derived). + User-provided message DummyClassTrackingToStringCalls + + expected type: System.String (or derived) + actual type: System.Int32 + + Assert.IsInstanceOfType(5) + """); o.WasToStringCalled.Should().BeTrue(); } @@ -108,14 +177,30 @@ public void IsInstanceOfTypeUsingGenericType_WhenValueIsNull_Fails() { Action action = () => Assert.IsInstanceOfType(null); action.Should().Throw() - .WithMessage($"Assertion failed. Expected value to be of type AssertTests (or derived).{Environment.NewLine}{Environment.NewLine}expected type: Microsoft.VisualStudio.TestPlatform.TestFramework.UnitTests.AssertTests (or derived){Environment.NewLine}actual: null{Environment.NewLine}{Environment.NewLine}Assert.IsInstanceOfType(null)"); + .WithMessage( + """ + Assertion failed. Expected value to be of type AssertTests (or derived). + + expected type: Microsoft.VisualStudio.TestPlatform.TestFramework.UnitTests.AssertTests (or derived) + actual: null + + Assert.IsInstanceOfType(null) + """); } public void IsInstanceOfTypeUsingGenericType_WhenTypeMismatch_Fails() { Action action = () => Assert.IsInstanceOfType(5); action.Should().Throw() - .WithMessage($"Assertion failed. Expected value to be of type String (or derived).{Environment.NewLine}{Environment.NewLine}expected type: System.String (or derived){Environment.NewLine}actual type: System.Int32{Environment.NewLine}actual value: 5{Environment.NewLine}{Environment.NewLine}Assert.IsInstanceOfType(5)"); + .WithMessage( + """ + Assertion failed. Expected value to be of type String (or derived). + + expected type: System.String (or derived) + actual type: System.Int32 + + Assert.IsInstanceOfType(5) + """); } public void IsInstanceOfTypeUsingGenericTypeWithOutParameter_WhenValueIsNull_Fails() @@ -224,6 +309,85 @@ public void IsNotInstanceOfType_WhenNonNullNullableTypeAndMessage_LearnNonNull() _ = intType.ToString(); // no warning about possible null } + public void IsNotInstanceOfType_OnExactType_ShouldFailWithActualTypeEvidence() + { + Action action = () => Assert.IsNotInstanceOfType(5, typeof(int)); + action.Should().Throw() + .WithMessage( + """ + Assertion failed. Expected value to not be of type Int32 (or derived). + + not expected type: System.Int32 (or derived) + actual type: System.Int32 + + Assert.IsNotInstanceOfType(5) + """); + } + + public void IsNotInstanceOfType_OnDerivedType_ShouldFailWithActualTypeEvidence() + { + object x = new MemoryStream(); + Action action = () => Assert.IsNotInstanceOfType(x, typeof(Stream)); + action.Should().Throw() + .WithMessage( + """ + Assertion failed. Expected value to not be of type Stream (or derived). + + not expected type: System.IO.Stream (or derived) + actual type: System.IO.MemoryStream + + Assert.IsNotInstanceOfType(x) + """); + } + + public void IsNotInstanceOfType_WhenTypeIsNull_ShouldFailWithDedicatedMessage() + { + Action action = () => Assert.IsNotInstanceOfType(5, null); + action.Should().Throw() + .WithMessage( + """ + Assertion failed. Cannot check type because the not-expected type argument is null. + + Assert.IsNotInstanceOfType(5) + """); + } + + public void IsInstanceOfType_OnFailure_ShouldPopulateExpectedAndActualPayload() + { + try + { + Assert.IsInstanceOfType(5, typeof(string)); + } + catch (AssertFailedException ex) + { + ex.ExpectedText.Should().Be("System.String (or derived)"); + ex.ActualText.Should().Be("System.Int32"); + ex.Data["assert.expected"].Should().Be("System.String (or derived)"); + ex.Data["assert.actual"].Should().Be("System.Int32"); + return; + } + + throw new AssertFailedException("Expected AssertFailedException was not thrown."); + } + + public void IsNotInstanceOfType_OnFailure_ShouldPopulateExpectedAndActualPayload() + { + try + { + Assert.IsNotInstanceOfType(5, typeof(int)); + } + catch (AssertFailedException ex) + { + ex.ExpectedText.Should().Be("System.Int32 (or derived)"); + ex.ActualText.Should().Be("System.Int32"); + ex.Data["assert.expected"].Should().Be("System.Int32 (or derived)"); + ex.Data["assert.actual"].Should().Be("System.Int32"); + return; + } + + throw new AssertFailedException("Expected AssertFailedException was not thrown."); + } + private object? GetObj() => new(); private Type? GetObjType() => typeof(object); diff --git a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.ScopeTests.cs b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.ScopeTests.cs index adc4ae9b12..ad5e7754da 100644 --- a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.ScopeTests.cs +++ b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.ScopeTests.cs @@ -160,11 +160,15 @@ public void Scope_AssertIsInstanceOfType_IsSoftFailure() .Which; innerException.InnerExceptions.Should().HaveCount(2); - string instanceOfTypeMessage = innerException.InnerExceptions[0].Message; - instanceOfTypeMessage.Should().Contain("Assertion failed. Expected value to be of type Int32 (or derived)."); - instanceOfTypeMessage.Should().Contain("expected type: System.Int32 (or derived)"); - instanceOfTypeMessage.Should().Contain("actual type: System.String"); - instanceOfTypeMessage.Should().Contain("Assert.IsInstanceOfType(value)"); + innerException.InnerExceptions[0].Message.Should().Be( + """ + Assertion failed. Expected value to be of type Int32 (or derived). + + expected type: System.Int32 (or derived) + actual type: System.String + + Assert.IsInstanceOfType(value) + """); innerException.InnerExceptions[1].Message.Should().Contain("Assert.AreEqual failed."); } @@ -187,11 +191,15 @@ public void Scope_AssertIsExactInstanceOfType_IsSoftFailure() .Which; innerException.InnerExceptions.Should().HaveCount(2); - string exactInstanceOfTypeMessage = innerException.InnerExceptions[0].Message; - exactInstanceOfTypeMessage.Should().Contain("Assertion failed. Expected value to be exactly of type Object."); - exactInstanceOfTypeMessage.Should().Contain("expected type: System.Object"); - exactInstanceOfTypeMessage.Should().Contain("actual type: System.String"); - exactInstanceOfTypeMessage.Should().Contain("Assert.IsExactInstanceOfType(value)"); + innerException.InnerExceptions[0].Message.Should().Be( + """ + Assertion failed. Expected value to be exactly of type Object. + + expected type: System.Object + actual type: System.String + + Assert.IsExactInstanceOfType(value) + """); innerException.InnerExceptions[1].Message.Should().Contain("Assert.AreEqual failed."); } diff --git a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertionValueRendererTests.cs b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertionValueRendererTests.cs index 20cbe332aa..bdbc362377 100644 --- a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertionValueRendererTests.cs +++ b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertionValueRendererTests.cs @@ -88,6 +88,10 @@ public void RenderValue_Char_ReturnsSingleQuoted() => public void RenderValue_CharNewline_ReturnsEscaped() => AssertionValueRenderer.RenderValue('\n').Should().Be("'\\n'"); + public void RenderValue_ObjectWhoseToStringThrows_ReturnsTypeAndExceptionName() => + AssertionValueRenderer.RenderValue(new ObjectWithThrowingToString()).Should().Be( + "Microsoft.VisualStudio.TestPlatform.TestFramework.UnitTests.AssertionValueRendererTests+ObjectWithThrowingToString (ToString threw InvalidOperationException)"); + private sealed class ObjectWithCustomToString { private readonly string _value; @@ -99,4 +103,9 @@ public ObjectWithCustomToString(string value) public override string ToString() => _value; } + + private sealed class ObjectWithThrowingToString + { + public override string ToString() => throw new InvalidOperationException("boom"); + } }