diff --git a/src/TestFramework/TestFramework/Assertions/Assert.cs b/src/TestFramework/TestFramework/Assertions/Assert.cs
index 0703444ae6..56b13403ec 100644
--- a/src/TestFramework/TestFramework/Assertions/Assert.cs
+++ b/src/TestFramework/TestFramework/Assertions/Assert.cs
@@ -107,6 +107,75 @@ 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,
+ };
+
+ if (structuredMessage.ExpectedText is not null)
+ {
+ exception.Data["assert.expected"] = structuredMessage.ExpectedText;
+ }
+
+ if (structuredMessage.ActualText is not null)
+ {
+ exception.Data["assert.actual"] = 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 - Deliberately keeping [DoesNotReturn] annotation while using soft assertions. Within an AssertScope, the postcondition is not enforced (same as all other assertion postconditions in scoped mode).
+ [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..e4a40a153c
--- /dev/null
+++ b/src/TestFramework/TestFramework/Assertions/AssertionValueRenderer.cs
@@ -0,0 +1,109 @@
+// 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;
+
+///
+/// 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)
+ => value switch
+ {
+ null => "null",
+ string s => RenderString(s),
+ bool b => b ? "true" : "false",
+ char c => RenderChar(c),
+ IEnumerable enumerable => 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..4bc26ac237
--- /dev/null
+++ b/src/TestFramework/TestFramework/Assertions/EvidenceBlock.cs
@@ -0,0 +1,62 @@
+// 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 sealed class EvidenceBlock
+{
+ private readonly List _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 (which includes trailing colon) to align values, then append a space 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..01b55290cf
--- /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 readonly List _evidenceBlocks = [];
+ private string? _userMessage;
+ 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)
+ {
+ _evidenceBlocks.Add(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 blocks (each separated by blank line)
+ foreach (EvidenceBlock evidence in _evidenceBlocks)
+ {
+ 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..2b740c88fa
--- /dev/null
+++ b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertFailedExceptionTests.cs
@@ -0,0 +1,87 @@
+// 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");
+ }
+
+ public void ThrowAssertFailed_WithStructuredMessage_PopulatesExpectedAndActualTextAndDataEntries()
+ {
+ EvidenceBlock evidence = EvidenceBlock.Create()
+ .AddLine("expected:", "42")
+ .AddLine("actual:", "37");
+
+ StructuredAssertionMessage message = new("Expected values to be equal.");
+ message.WithEvidence(evidence);
+ message.WithExpectedAndActual("42", "37");
+
+ AssertFailedException? caught = null;
+ try
+ {
+ Assert.ThrowAssertFailed(message);
+ }
+ catch (AssertFailedException ex)
+ {
+ caught = ex;
+ }
+
+ caught.Should().NotBeNull();
+ caught!.ExpectedText.Should().Be("42");
+ caught.ActualText.Should().Be("37");
+ caught.Data["assert.expected"].Should().Be("42");
+ caught.Data["assert.actual"].Should().Be("37");
+ }
+
+ public void ThrowAssertFailed_WithStructuredMessage_NullExpectedAndActual_DoesNotPopulateDataEntries()
+ {
+ StructuredAssertionMessage message = new("Condition failed.");
+
+ AssertFailedException? caught = null;
+ try
+ {
+ Assert.ThrowAssertFailed(message);
+ }
+ catch (AssertFailedException ex)
+ {
+ caught = ex;
+ }
+
+ caught.Should().NotBeNull();
+ caught!.ExpectedText.Should().BeNull();
+ caught.ActualText.Should().BeNull();
+ caught.Data.Contains("assert.expected").Should().BeFalse();
+ caught.Data.Contains("assert.actual").Should().BeFalse();
+ }
+}
diff --git a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertionValueRendererTests.cs b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertionValueRendererTests.cs
new file mode 100644
index 0000000000..20cbe332aa
--- /dev/null
+++ b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertionValueRendererTests.cs
@@ -0,0 +1,102 @@
+// 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.Globalization;
+
+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.ToString(CultureInfo.CurrentCulture));
+
+ 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..c8c6857665
--- /dev/null
+++ b/test/UnitTests/TestFramework.UnitTests/Assertions/StructuredAssertionMessageTests.cs
@@ -0,0 +1,201 @@
+// 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();
+
+ result.Should().Be(
+ """
+ Assertion failed. Expected values to be equal.
+ Discount should be applied after tax
+ """);
+ }
+
+ 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();
+
+ result.Should().Be(
+ """
+ Assertion failed. Expected values to be equal.
+
+ expected: 42
+ actual: 37
+ """);
+ }
+
+ 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();
+
+ result.Should().Be(
+ """
+ Assertion failed. Expected values to be equal.
+ Discount should be applied after tax
+
+ expected: 42
+ actual: 37
+ """);
+ }
+
+ 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();
+
+ result.Should().Be(
+ """
+ Assertion failed. Expected values to be equal.
+
+ expected: 42
+ actual: 37
+
+ Assert.AreEqual(expectedCount, actualCount)
+ """);
+ }
+
+ 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();
+
+ result.Should().Be(
+ """
+ Assertion failed. Expected values to be equal.
+ Values differ at position 3.
+ Check the discount logic
+
+ expected: 42
+ actual: 37
+
+ Assert.AreEqual(expected, actual)
+ """);
+ }
+
+ 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());
+ }
+
+ public void Format_WithMultipleEvidenceBlocks_SeparatedByBlankLines()
+ {
+ EvidenceBlock evidence1 = EvidenceBlock.Create()
+ .AddLine("expected:", "42")
+ .AddLine("actual:", "37");
+
+ EvidenceBlock evidence2 = EvidenceBlock.Create()
+ .AddLine("ignore case:", "true")
+ .AddLine("culture:", "tr-TR");
+
+ StructuredAssertionMessage message = new("Expected values to be equal.");
+ message.WithEvidence(evidence1);
+ message.WithEvidence(evidence2);
+
+ string result = message.Format();
+
+ result.Should().Be(
+ """
+ Assertion failed. Expected values to be equal.
+
+ expected: 42
+ actual: 37
+
+ ignore case: true
+ culture: tr-TR
+ """);
+ }
+}