diff --git a/docs/RFCs/012-Structured-Assertion-Messages.md b/docs/RFCs/012-Structured-Assertion-Messages.md index b504dff883..d697a9be12 100644 --- a/docs/RFCs/012-Structured-Assertion-Messages.md +++ b/docs/RFCs/012-Structured-Assertion-Messages.md @@ -307,8 +307,9 @@ Assert.ThrowsExactly(() => Validate(input)) Assertion failed. Expected exception of exact type ArgumentException but caught InvalidOperationException. expected type: System.ArgumentException -actual type: System.InvalidOperationException actual exception: System.InvalidOperationException: Operation is not valid due to the current state of the object. + at MyApp.Service.Validate(String input) in Service.cs:line 42 + at MyTests.ValidationTests.InvalidInput_ShouldThrow() in ValidationTests.cs:line 18 Assert.ThrowsExactly(() => Validate(input)) at MyTests.ValidationTests.InvalidInput_ShouldThrow() in ValidationTests.cs:line 18 @@ -679,8 +680,9 @@ Assertion failed. Expected exception of type ArgumentException (or derived) but Assertion failed. Expected exception of type ArgumentException (or derived) but caught InvalidOperationException. expected type: System.ArgumentException (or derived) -actual type: System.InvalidOperationException actual exception: System.InvalidOperationException: Operation is not valid due to the current state of the object. + at MyApp.Service.Validate(String input) in Service.cs:line 42 + at MyTests.ValidationTests.InvalidInput_ShouldThrow() in ValidationTests.cs:line 18 ``` #### Assert.ThrowsExactly (no exception thrown) @@ -695,8 +697,9 @@ Assertion failed. Expected exception of exact type ArgumentException but no exce Assertion failed. Expected exception of exact type ArgumentException but caught ArgumentNullException. expected type: System.ArgumentException -actual type: System.ArgumentNullException actual exception: System.ArgumentNullException: Value cannot be null. + at MyApp.Service.Validate(String input) in Service.cs:line 42 + at MyTests.ValidationTests.InvalidInput_ShouldThrow() in ValidationTests.cs:line 18 ``` #### Assert.ThrowsAsync / Assert.ThrowsExactlyAsync diff --git a/src/TestFramework/TestFramework/Assertions/Assert.ThrowsException.Core.cs b/src/TestFramework/TestFramework/Assertions/Assert.ThrowsException.Core.cs index a81c307541..5e095e2cd0 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.ThrowsException.Core.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.ThrowsException.Core.cs @@ -172,7 +172,6 @@ private static void ReportThrowsFailed( Exception actualException = state.ExceptionThrown!; Type actualType = actualException.GetType(); string actualTypeName = GetDisplayTypeName(actualType, includeNamespace: false); - string actualTypeFullName = GetDisplayTypeName(actualType, includeNamespace: true); string summary = isStrictType ? $"Expected exception of exact type {expectedTypeName} but caught {actualTypeName}." @@ -180,11 +179,21 @@ private static void ReportThrowsFailed( string expectedTypeLabel = isStrictType ? expectedTypeFullName : $"{expectedTypeFullName} (or derived)"; - // The "actual exception:" line is already prefixed with the exception type name, so we don't emit a - // separate "actual type:" line to avoid duplicating that information. + // Render the full exception (type, message, inner-exception chain and stack trace) via ToString so the + // unexpected exception can be diagnosed without re-running under a debugger. See issue #9190. + // Exception.ToString() prefixes the output with Type.ToString(), which uses CLR notation for generic + // types (e.g. "MyException`1[System.Int32]"). Replace that leading prefix with the friendly name so it + // stays consistent with the "expected type:" line; for non-generic types the two notations are identical. + string actualExceptionText = actualException.ToString(); + string clrTypeName = actualType.ToString(); + if (actualExceptionText.StartsWith(clrTypeName, StringComparison.Ordinal)) + { + actualExceptionText = GetDisplayTypeName(actualType, includeNamespace: true) + actualExceptionText.Substring(clrTypeName.Length); + } + EvidenceBlock evidence = EvidenceBlock.Create() .AddLine("expected type:", expectedTypeLabel) - .AddLine("actual exception:", $"{actualTypeFullName}: {actualException.Message}"); + .AddLine("actual exception:", actualExceptionText); message = new StructuredAssertionMessage(summary) .WithUserMessage(userMessage) diff --git a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.ThrowsExceptionTests.cs b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.ThrowsExceptionTests.cs index 7b3c4a0749..b5b6f59909 100644 --- a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.ThrowsExceptionTests.cs +++ b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.ThrowsExceptionTests.cs @@ -70,12 +70,12 @@ public void ThrowsAsync_WhenExceptionIsNotExpectedType_ShouldThrow() Action action = t.Wait; action.Should().Throw() .WithInnerException() - .Which.Message.Should().Be( + .Which.Message.Should().Match( """ Assertion failed. Expected exception of type ArgumentException (or derived) but caught Exception. expected type: System.ArgumentException (or derived) - actual exception: System.Exception: Exception of type 'System.Exception' was thrown. + actual exception: System.Exception: Exception of type 'System.Exception' was thrown.* Assert.ThrowsAsync(() => throw new Exception()) """); @@ -87,12 +87,12 @@ public void ThrowsExactlyAsync_WhenExceptionIsDerivedFromExpectedType_ShouldThro Action action = t.Wait; action.Should().Throw() .WithInnerException() - .Which.Message.Should().Be( + .Which.Message.Should().Match( """ Assertion failed. Expected exception of exact type ArgumentException but caught ArgumentNullException. expected type: System.ArgumentException - actual exception: System.ArgumentNullException: Value cannot be null. + actual exception: System.ArgumentNullException: Value cannot be null.* Assert.ThrowsExactlyAsync(() => throw new ArgumentNullException()) """); @@ -144,23 +144,13 @@ public void Throws_WithMessageBuilder_FailsBecauseTypeMismatch() return "message constructed via builder."; }); action.Should().Throw() - .Which.Message.Should().BeOneOf( - """ - Assertion failed. Expected exception of type ArgumentNullException (or derived) but caught ArgumentOutOfRangeException. - message constructed via builder. - - expected type: System.ArgumentNullException (or derived) - actual exception: System.ArgumentOutOfRangeException: Specified argument was out of the range of valid values. (Parameter 'MyParamNameHere') - - Assert.Throws(() => throw new ArgumentOutOfRangeException("MyParamNameHere")) - """, + .Which.Message.Should().Match( """ Assertion failed. Expected exception of type ArgumentNullException (or derived) but caught ArgumentOutOfRangeException. message constructed via builder. expected type: System.ArgumentNullException (or derived) - actual exception: System.ArgumentOutOfRangeException: Specified argument was out of the range of valid values. - Parameter name: MyParamNameHere + actual exception: System.ArgumentOutOfRangeException: Specified argument was out of the range of valid values.*MyParamNameHere* Assert.Throws(() => throw new ArgumentOutOfRangeException("MyParamNameHere")) """); @@ -216,23 +206,13 @@ public void ThrowsExactly_WithMessageBuilder_FailsBecauseTypeMismatch() return "message constructed via builder."; }); action.Should().Throw() - .Which.Message.Should().BeOneOf( + .Which.Message.Should().Match( """ Assertion failed. Expected exception of exact type ArgumentNullException but caught ArgumentOutOfRangeException. message constructed via builder. expected type: System.ArgumentNullException - actual exception: System.ArgumentOutOfRangeException: Specified argument was out of the range of valid values. (Parameter 'MyParamNameHere') - - Assert.ThrowsExactly(() => throw new ArgumentOutOfRangeException("MyParamNameHere")) - """, - """ - Assertion failed. Expected exception of exact type ArgumentNullException but caught ArgumentOutOfRangeException. - message constructed via builder. - - expected type: System.ArgumentNullException - actual exception: System.ArgumentOutOfRangeException: Specified argument was out of the range of valid values. - Parameter name: MyParamNameHere + actual exception: System.ArgumentOutOfRangeException: Specified argument was out of the range of valid values.*MyParamNameHere* Assert.ThrowsExactly(() => throw new ArgumentOutOfRangeException("MyParamNameHere")) """); @@ -288,23 +268,13 @@ public async Task ThrowsAsync_WithMessageBuilder_FailsBecauseTypeMismatch() return "message constructed via builder."; }); (await action.Should().ThrowAsync()) - .Which.Message.Should().BeOneOf( + .Which.Message.Should().Match( """ Assertion failed. Expected exception of type ArgumentNullException (or derived) but caught ArgumentOutOfRangeException. message constructed via builder. expected type: System.ArgumentNullException (or derived) - actual exception: System.ArgumentOutOfRangeException: Specified argument was out of the range of valid values. (Parameter 'MyParamNameHere') - - Assert.ThrowsAsync(() => Task.FromException(new ArgumentOutOfRangeException("MyParamNameHere"))) - """, - """ - Assertion failed. Expected exception of type ArgumentNullException (or derived) but caught ArgumentOutOfRangeException. - message constructed via builder. - - expected type: System.ArgumentNullException (or derived) - actual exception: System.ArgumentOutOfRangeException: Specified argument was out of the range of valid values. - Parameter name: MyParamNameHere + actual exception: System.ArgumentOutOfRangeException: Specified argument was out of the range of valid values.*MyParamNameHere* Assert.ThrowsAsync(() => Task.FromException(new ArgumentOutOfRangeException("MyParamNameHere"))) """); @@ -360,23 +330,13 @@ public async Task ThrowsExactlyAsync_WithMessageBuilder_FailsBecauseTypeMismatch return "message constructed via builder."; }); (await action.Should().ThrowAsync()) - .Which.Message.Should().BeOneOf( - """ - Assertion failed. Expected exception of exact type ArgumentNullException but caught ArgumentOutOfRangeException. - message constructed via builder. - - expected type: System.ArgumentNullException - actual exception: System.ArgumentOutOfRangeException: Specified argument was out of the range of valid values. (Parameter 'MyParamNameHere') - - Assert.ThrowsExactlyAsync(() => Task.FromException(new ArgumentOutOfRangeException("MyParamNameHere"))) - """, + .Which.Message.Should().Match( """ Assertion failed. Expected exception of exact type ArgumentNullException but caught ArgumentOutOfRangeException. message constructed via builder. expected type: System.ArgumentNullException - actual exception: System.ArgumentOutOfRangeException: Specified argument was out of the range of valid values. - Parameter name: MyParamNameHere + actual exception: System.ArgumentOutOfRangeException: Specified argument was out of the range of valid values.*MyParamNameHere* Assert.ThrowsExactlyAsync(() => Task.FromException(new ArgumentOutOfRangeException("MyParamNameHere"))) """); @@ -421,13 +381,13 @@ public void Throws_WhenExceptionMessageContainsNewline_ContinuationLinesAreInden // "actual exception:" is the longest label (17 chars) + 1 space = 18 chars indent for the continuation line. action.Should().Throw() - .Which.Message.Should().Be( + .Which.Message.Should().Match( """ Assertion failed. Expected exception of type ArgumentException (or derived) but caught InvalidOperationException. expected type: System.ArgumentException (or derived) actual exception: System.InvalidOperationException: line1 - line2 + line2* Assert.Throws(() => throw new InvalidOperationException("line1\nline2")) """); @@ -445,12 +405,12 @@ static void Action() => Assert.Throws(() => Action action = Action; action.Should().Throw() - .Which.Message.Should().Be( + .Which.Message.Should().Match( """ Assertion failed. Expected exception of type ArgumentException (or derived) but caught InvalidOperationException. expected type: System.ArgumentException (or derived) - actual exception: System.InvalidOperationException: oops + actual exception: System.InvalidOperationException: oops* Assert.Throws() """); @@ -464,12 +424,12 @@ public void Throws_WhenExpectedTypeIsGeneric_RendersFriendlyTypeName() Action action = Action; action.Should().Throw() - .Which.Message.Should().Be( + .Which.Message.Should().Match( """ Assertion failed. Expected exception of type ThrowsTestGenericException (or derived) but caught InvalidOperationException. expected type: Microsoft.VisualStudio.TestPlatform.TestFramework.UnitTests.ThrowsTestGenericException (or derived) - actual exception: System.InvalidOperationException: oops + actual exception: System.InvalidOperationException: oops* Assert.Throws>(() => throw new InvalidOperationException("oops")) """);