From 7909bfc62d60dadea83a16f966cf626dedd019d8 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Sun, 21 Sep 2025 19:45:29 +0100 Subject: [PATCH] fix: add WithMessageNotContaining assertion for exception messages --- .../ThrowInDelegateValueAssertionTests.cs | 64 +++++++++++++++++++ .../Assertions/Throws/ThrowsException.cs | 14 ++++ ...WithMessageNotContainingAssertCondition.cs | 28 ++++++++ ..._Has_No_API_Changes.DotNet8_0.verified.txt | 9 +++ ..._Has_No_API_Changes.DotNet9_0.verified.txt | 9 +++ ...ary_Has_No_API_Changes.Net4_7.verified.txt | 9 +++ 6 files changed, 133 insertions(+) create mode 100644 TUnit.Assertions/Assertions/Throws/ThrowsWithMessageNotContainingAssertCondition.cs diff --git a/TUnit.Assertions.Tests/ThrowInDelegateValueAssertionTests.cs b/TUnit.Assertions.Tests/ThrowInDelegateValueAssertionTests.cs index 97668c73dd..1e5dca0de7 100644 --- a/TUnit.Assertions.Tests/ThrowInDelegateValueAssertionTests.cs +++ b/TUnit.Assertions.Tests/ThrowInDelegateValueAssertionTests.cs @@ -37,4 +37,68 @@ await Assert.That(assertion) .Throws() .WithMessageContaining("SYSTEM.EXCEPTION", StringComparison.OrdinalIgnoreCase); } + + [Test] + public async Task ThrowInDelegateValueAssertion_WithMessageNotContaining_Passes_WhenMessageDoesNotContainText() + { + var assertion = async () => await Assert.That(() => + { + throw new Exception("This is an error message"); + return true; + }).IsEqualTo(true); + + await Assert.That(assertion) + .Throws() + .WithMessageNotContaining("different text"); + } + + [Test] + public async Task ThrowInDelegateValueAssertion_WithMessageNotContaining_Fails_WhenMessageContainsText() + { + var assertion = async () => await Assert.That(() => + { + throw new Exception("This is an error message"); + return true; + }).IsEqualTo(true); + + var finalAssertion = async () => await Assert.That(assertion) + .Throws() + .WithMessageNotContaining("error message"); + + await Assert.That(finalAssertion) + .Throws() + .WithMessageContaining("which message does not contain \"error message\""); + } + + [Test] + public async Task ThrowInDelegateValueAssertion_WithMessageNotContaining_RespectsCaseInsensitive() + { + var assertion = async () => await Assert.That(() => + { + throw new Exception("This is an ERROR message"); + return true; + }).IsEqualTo(true); + + var finalAssertion = async () => await Assert.That(assertion) + .Throws() + .WithMessageNotContaining("error message", StringComparison.OrdinalIgnoreCase); + + await Assert.That(finalAssertion) + .Throws() + .WithMessageContaining("which message does not contain \"error message\""); + } + + [Test] + public async Task ThrowInDelegateValueAssertion_WithMessageNotContaining_RespectsCaseSensitive() + { + var assertion = async () => await Assert.That(() => + { + throw new Exception("This is an ERROR message"); + return true; + }).IsEqualTo(true); + + await Assert.That(assertion) + .Throws() + .WithMessageNotContaining("error message", StringComparison.Ordinal); + } } diff --git a/TUnit.Assertions/Assertions/Throws/ThrowsException.cs b/TUnit.Assertions/Assertions/Throws/ThrowsException.cs index 6b8807dc0a..b2cdac45a8 100644 --- a/TUnit.Assertions/Assertions/Throws/ThrowsException.cs +++ b/TUnit.Assertions/Assertions/Throws/ThrowsException.cs @@ -50,6 +50,20 @@ public ThrowsException WithMessageContaining(string expecte return this; } + public ThrowsException WithMessageNotContaining(string expected, [CallerArgumentExpression(nameof(expected))] string? doNotPopulateThisValue = null) + { + _source.RegisterAssertion(new ThrowsWithMessageNotContainingAssertCondition(expected, StringComparison.Ordinal, _selector) + , [doNotPopulateThisValue]); + return this; + } + + public ThrowsException WithMessageNotContaining(string expected, StringComparison stringComparison, [CallerArgumentExpression(nameof(expected))] string? doNotPopulateThisValue = null, [CallerArgumentExpression(nameof(stringComparison))] string? doNotPopulateThisValue2 = null) + { + _source.RegisterAssertion(new ThrowsWithMessageNotContainingAssertCondition(expected, stringComparison, _selector) + , [doNotPopulateThisValue, doNotPopulateThisValue2]); + return this; + } + public ThrowsException WithInnerException() { _source.AppendExpression($"{nameof(WithInnerException)}()"); diff --git a/TUnit.Assertions/Assertions/Throws/ThrowsWithMessageNotContainingAssertCondition.cs b/TUnit.Assertions/Assertions/Throws/ThrowsWithMessageNotContainingAssertCondition.cs new file mode 100644 index 0000000000..a5c6565745 --- /dev/null +++ b/TUnit.Assertions/Assertions/Throws/ThrowsWithMessageNotContainingAssertCondition.cs @@ -0,0 +1,28 @@ +using TUnit.Assertions.Extensions; + +namespace TUnit.Assertions.AssertConditions.Throws; + +public class ThrowsWithMessageNotContainingAssertCondition( + string expected, + StringComparison stringComparison, + Func exceptionSelector) + : DelegateAssertCondition + where TException : Exception +{ + internal protected override string GetExpectation() + => $"to throw {typeof(TException).Name.PrependAOrAn()} which message does not contain \"{expected?.ShowNewLines().TruncateWithEllipsis(100)}\""; + + protected override ValueTask GetResult( + TActual? actualValue, Exception? exception, + AssertionMetadata assertionMetadata + ) + { + var actualException = exceptionSelector(exception); + + return AssertionResult + .FailIf(actualException is null, + "the exception is null") + .OrFailIf(actualException is not null && actualException.Message.Contains(expected, stringComparison), + $"found \"{actualException?.Message.ShowNewLines().TruncateWithEllipsis(100)}\""); + } +} \ No newline at end of file diff --git a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet8_0.verified.txt b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet8_0.verified.txt index 4093e641bc..64cc4cc27d 100644 --- a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet8_0.verified.txt +++ b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet8_0.verified.txt @@ -625,6 +625,13 @@ namespace . protected override string GetExpectation() { } protected override .<.> GetResult(TActual? actualValue, ? exception, .AssertionMetadata assertionMetadata) { } } + public class ThrowsWithMessageNotContainingAssertCondition : . + where TException : + { + public ThrowsWithMessageNotContainingAssertCondition(string expected, stringComparison, exceptionSelector) { } + protected override string GetExpectation() { } + protected override .<.> GetResult(TActual? actualValue, ? exception, .AssertionMetadata assertionMetadata) { } + } public class ThrowsWithParamNameAssertCondition : . where TException : { @@ -2734,6 +2741,8 @@ namespace .Extensions public . WithMessageContaining(string expected, [.("expected")] string? doNotPopulateThisValue = null) { } public . WithMessageContaining(string expected, stringComparison, [.("expected")] string? doNotPopulateThisValue = null, [.("stringComparison")] string? doNotPopulateThisValue2 = null) { } public . WithMessageMatching(. match, [.("match")] string? doNotPopulateThisValue = null) { } + public . WithMessageNotContaining(string expected, [.("expected")] string? doNotPopulateThisValue = null) { } + public . WithMessageNotContaining(string expected, stringComparison, [.("expected")] string? doNotPopulateThisValue = null, [.("stringComparison")] string? doNotPopulateThisValue2 = null) { } public static . op_Explicit(. throwsException) { } } public static class ThrowsExtensions diff --git a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet9_0.verified.txt b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet9_0.verified.txt index 64b4574e68..38e1e51ae2 100644 --- a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet9_0.verified.txt +++ b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet9_0.verified.txt @@ -625,6 +625,13 @@ namespace . protected override string GetExpectation() { } protected override .<.> GetResult(TActual? actualValue, ? exception, .AssertionMetadata assertionMetadata) { } } + public class ThrowsWithMessageNotContainingAssertCondition : . + where TException : + { + public ThrowsWithMessageNotContainingAssertCondition(string expected, stringComparison, exceptionSelector) { } + protected override string GetExpectation() { } + protected override .<.> GetResult(TActual? actualValue, ? exception, .AssertionMetadata assertionMetadata) { } + } public class ThrowsWithParamNameAssertCondition : . where TException : { @@ -2734,6 +2741,8 @@ namespace .Extensions public . WithMessageContaining(string expected, [.("expected")] string? doNotPopulateThisValue = null) { } public . WithMessageContaining(string expected, stringComparison, [.("expected")] string? doNotPopulateThisValue = null, [.("stringComparison")] string? doNotPopulateThisValue2 = null) { } public . WithMessageMatching(. match, [.("match")] string? doNotPopulateThisValue = null) { } + public . WithMessageNotContaining(string expected, [.("expected")] string? doNotPopulateThisValue = null) { } + public . WithMessageNotContaining(string expected, stringComparison, [.("expected")] string? doNotPopulateThisValue = null, [.("stringComparison")] string? doNotPopulateThisValue2 = null) { } public static . op_Explicit(. throwsException) { } } public static class ThrowsExtensions diff --git a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.Net4_7.verified.txt b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.Net4_7.verified.txt index 4f9cdc57f3..df44314039 100644 --- a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.Net4_7.verified.txt +++ b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.Net4_7.verified.txt @@ -603,6 +603,13 @@ namespace . protected override string GetExpectation() { } protected override .<.> GetResult(TActual? actualValue, ? exception, .AssertionMetadata assertionMetadata) { } } + public class ThrowsWithMessageNotContainingAssertCondition : . + where TException : + { + public ThrowsWithMessageNotContainingAssertCondition(string expected, stringComparison, exceptionSelector) { } + protected override string GetExpectation() { } + protected override .<.> GetResult(TActual? actualValue, ? exception, .AssertionMetadata assertionMetadata) { } + } public class ThrowsWithParamNameAssertCondition : . where TException : { @@ -2590,6 +2597,8 @@ namespace .Extensions public . WithMessageContaining(string expected, [.("expected")] string? doNotPopulateThisValue = null) { } public . WithMessageContaining(string expected, stringComparison, [.("expected")] string? doNotPopulateThisValue = null, [.("stringComparison")] string? doNotPopulateThisValue2 = null) { } public . WithMessageMatching(. match, [.("match")] string? doNotPopulateThisValue = null) { } + public . WithMessageNotContaining(string expected, [.("expected")] string? doNotPopulateThisValue = null) { } + public . WithMessageNotContaining(string expected, stringComparison, [.("expected")] string? doNotPopulateThisValue = null, [.("stringComparison")] string? doNotPopulateThisValue2 = null) { } public static . op_Explicit(. throwsException) { } } public static class ThrowsExtensions