Allow WithoutMessage when using Should().Throw() and ThrowAsync() by dennisdoomen · Pull Request #3100 · fluentassertions/fluentassertions · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions Src/FluentAssertions/Equivalency/Steps/AssertionResultSet.cs
24 changes: 24 additions & 0 deletions Src/FluentAssertions/ExceptionAssertionsExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,30 @@ public static async Task<ExceptionAssertions<TException>> WithMessage<TException
return (await task).WithMessage(expectedWildcardPattern, because, becauseArgs);
}

/// <summary>
/// Asserts that the thrown exception does NOT have a message that matches <paramref name="wildcardPattern" />.
/// </summary>
/// <param name="task">The <see cref="ExceptionAssertions{TException}"/> containing the thrown exception.</param>
/// <param name="wildcardPattern">
/// The wildcard pattern with which the exception message is matched, where * and ? have special meanings.
/// </param>
/// <param name="because">
/// A formatted phrase as is supported by <see cref="string.Format(string,object[])" /> explaining why the assertion
/// is needed. If the phrase does not start with the word <i>because</i>, it is prepended automatically.
/// </param>
/// <param name="becauseArgs">
/// Zero or more objects to format using the placeholders in <paramref name="because"/>.
/// </param>
public static async Task<ExceptionAssertions<TException>> WithoutMessage<TException>(
this Task<ExceptionAssertions<TException>> task,
string wildcardPattern,
[StringSyntax("CompositeFormat")] string because = "",
params object[] becauseArgs)
where TException : Exception
{
return (await task).WithoutMessage(wildcardPattern, because, becauseArgs);
}

/// <summary>
/// Asserts that the exception matches a particular condition.
/// </summary>
Expand Down
58 changes: 54 additions & 4 deletions Src/FluentAssertions/Specialized/ExceptionAssertions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,24 +84,74 @@ public virtual ExceptionAssertions<TException> WithMessage(string expectedWildca
.ForCondition(Subject.Any())
.FailWith("Expected exception with message {0}{reason}, but no exception was thrown.", expectedWildcardPattern);

AssertExceptionMessage(expectedWildcardPattern, because, becauseArgs);
AssertExceptionMessage(message =>
message.Should().MatchEquivalentOf(expectedWildcardPattern, because, becauseArgs));

return this;
}

private void AssertExceptionMessage(string expectedWildcardPattern, string because, object[] becauseArgs)
/// <summary>
/// Asserts that the thrown exception does NOT have a message that matches <paramref name="wildcardPattern" />.
/// </summary>
/// <param name="wildcardPattern">
/// The pattern to match against the exception message. This parameter can contain a combination of literal text and
/// wildcard (* and ?) characters, but it doesn't support regular expressions.
/// </param>
/// <param name="because">
/// A formatted phrase as is supported by <see cref="string.Format(string,object[])" /> explaining why the assertion
/// is needed. If the phrase does not start with the word <i>because</i>, it is prepended automatically.
/// </param>
/// <param name="becauseArgs">
/// Zero or more objects to format using the placeholders in <paramref name="because"/>.
/// </param>
/// <remarks>
/// <paramref name="wildcardPattern"/> can be a combination of literal and wildcard characters,
/// but it doesn't support regular expressions. The following wildcard specifiers are permitted in
/// <paramref name="wildcardPattern"/>.
/// <list type="table">
/// <listheader>
/// <term>Wildcard character</term>
/// <description>Description</description>
/// </listheader>
/// <item>
/// <term>* (asterisk)</term>
/// <description>Zero or more characters in that position.</description>
/// </item>
/// <item>
/// <term>? (question mark)</term>
/// <description>Exactly one character in that position.</description>
/// </item>
/// </list>
/// </remarks>
public virtual ExceptionAssertions<TException> WithoutMessage(string wildcardPattern,
[StringSyntax("CompositeFormat")] string because = "", params object[] becauseArgs)
{
assertionChain
.BecauseOf(because, becauseArgs)
.UsingLineBreaks
.ForCondition(Subject.Any())
Comment thread
dennisdoomen marked this conversation as resolved.
.FailWith("Expected exception without message matching {0}{reason}, but no exception was thrown.", wildcardPattern);

AssertExceptionMessage(message =>
message.Should().NotMatchEquivalentOf(wildcardPattern, because, becauseArgs));
Comment thread
dennisdoomen marked this conversation as resolved.

return this;
}

private void AssertExceptionMessage(Action<string> messageAssertion)
{
var results = new AssertionResultSet();

foreach (string message in Subject.Select(exc => exc.Message))
{
using (var scope = new AssertionScope())
{
// Treat every assertion within the scope as a new independent one.
var chain = AssertionChain.GetOrCreate();
chain.OverrideCallerIdentifier(() => "exception message");
chain.OverrideCallerIdentifier(() => "the exception message");
chain.ReuseOnce();

message.Should().MatchEquivalentOf(expectedWildcardPattern, because, becauseArgs);
messageAssertion(message);

results.AddSet(message, scope.Discard());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,8 @@ namespace FluentAssertions
where TException : System.ArgumentException { }
public static System.Threading.Tasks.Task<FluentAssertions.Specialized.ExceptionAssertions<TException>> WithParameterName<TException>(this System.Threading.Tasks.Task<FluentAssertions.Specialized.ExceptionAssertions<TException>> task, string paramName, string because = "", params object[] becauseArgs)
where TException : System.ArgumentException { }
public static System.Threading.Tasks.Task<FluentAssertions.Specialized.ExceptionAssertions<TException>> WithoutMessage<TException>(this System.Threading.Tasks.Task<FluentAssertions.Specialized.ExceptionAssertions<TException>> task, string wildcardPattern, string because = "", params object[] becauseArgs)
where TException : System.Exception { }
}
public static class FluentActions
{
Expand Down Expand Up @@ -2166,6 +2168,7 @@ namespace FluentAssertions.Specialized
public virtual FluentAssertions.Specialized.ExceptionAssertions<TInnerException> WithInnerExceptionExactly<TInnerException>(string because = "", params object[] becauseArgs)
where TInnerException : System.Exception { }
public virtual FluentAssertions.Specialized.ExceptionAssertions<TException> WithMessage(string expectedWildcardPattern, string because = "", params object[] becauseArgs) { }
public virtual FluentAssertions.Specialized.ExceptionAssertions<TException> WithoutMessage(string wildcardPattern, string because = "", params object[] becauseArgs) { }
}
public class ExecutionTime
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,8 @@ namespace FluentAssertions
where TException : System.ArgumentException { }
public static System.Threading.Tasks.Task<FluentAssertions.Specialized.ExceptionAssertions<TException>> WithParameterName<TException>(this System.Threading.Tasks.Task<FluentAssertions.Specialized.ExceptionAssertions<TException>> task, string paramName, string because = "", params object[] becauseArgs)
where TException : System.ArgumentException { }
public static System.Threading.Tasks.Task<FluentAssertions.Specialized.ExceptionAssertions<TException>> WithoutMessage<TException>(this System.Threading.Tasks.Task<FluentAssertions.Specialized.ExceptionAssertions<TException>> task, string wildcardPattern, string because = "", params object[] becauseArgs)
where TException : System.Exception { }
}
public static class FluentActions
{
Expand Down Expand Up @@ -2301,6 +2303,7 @@ namespace FluentAssertions.Specialized
public virtual FluentAssertions.Specialized.ExceptionAssertions<TInnerException> WithInnerExceptionExactly<TInnerException>(string because = "", params object[] becauseArgs)
where TInnerException : System.Exception { }
public virtual FluentAssertions.Specialized.ExceptionAssertions<TException> WithMessage(string expectedWildcardPattern, string because = "", params object[] becauseArgs) { }
public virtual FluentAssertions.Specialized.ExceptionAssertions<TException> WithoutMessage(string wildcardPattern, string because = "", params object[] becauseArgs) { }
}
public class ExecutionTime
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,8 @@ namespace FluentAssertions
where TException : System.ArgumentException { }
public static System.Threading.Tasks.Task<FluentAssertions.Specialized.ExceptionAssertions<TException>> WithParameterName<TException>(this System.Threading.Tasks.Task<FluentAssertions.Specialized.ExceptionAssertions<TException>> task, string paramName, string because = "", params object[] becauseArgs)
where TException : System.ArgumentException { }
public static System.Threading.Tasks.Task<FluentAssertions.Specialized.ExceptionAssertions<TException>> WithoutMessage<TException>(this System.Threading.Tasks.Task<FluentAssertions.Specialized.ExceptionAssertions<TException>> task, string wildcardPattern, string because = "", params object[] becauseArgs)
where TException : System.Exception { }
}
public static class FluentActions
{
Expand Down Expand Up @@ -2110,6 +2112,7 @@ namespace FluentAssertions.Specialized
public virtual FluentAssertions.Specialized.ExceptionAssertions<TInnerException> WithInnerExceptionExactly<TInnerException>(string because = "", params object[] becauseArgs)
where TInnerException : System.Exception { }
public virtual FluentAssertions.Specialized.ExceptionAssertions<TException> WithMessage(string expectedWildcardPattern, string because = "", params object[] becauseArgs) { }
public virtual FluentAssertions.Specialized.ExceptionAssertions<TException> WithoutMessage(string wildcardPattern, string because = "", params object[] becauseArgs) { }
}
public class ExecutionTime
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,8 @@ namespace FluentAssertions
where TException : System.ArgumentException { }
public static System.Threading.Tasks.Task<FluentAssertions.Specialized.ExceptionAssertions<TException>> WithParameterName<TException>(this System.Threading.Tasks.Task<FluentAssertions.Specialized.ExceptionAssertions<TException>> task, string paramName, string because = "", params object[] becauseArgs)
where TException : System.ArgumentException { }
public static System.Threading.Tasks.Task<FluentAssertions.Specialized.ExceptionAssertions<TException>> WithoutMessage<TException>(this System.Threading.Tasks.Task<FluentAssertions.Specialized.ExceptionAssertions<TException>> task, string wildcardPattern, string because = "", params object[] becauseArgs)
where TException : System.Exception { }
}
public static class FluentActions
{
Expand Down Expand Up @@ -2166,6 +2168,7 @@ namespace FluentAssertions.Specialized
public virtual FluentAssertions.Specialized.ExceptionAssertions<TInnerException> WithInnerExceptionExactly<TInnerException>(string because = "", params object[] becauseArgs)
where TInnerException : System.Exception { }
public virtual FluentAssertions.Specialized.ExceptionAssertions<TException> WithMessage(string expectedWildcardPattern, string because = "", params object[] becauseArgs) { }
public virtual FluentAssertions.Specialized.ExceptionAssertions<TException> WithoutMessage(string wildcardPattern, string because = "", params object[] becauseArgs) { }
}
public class ExecutionTime
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

namespace FluentAssertions.Specs.Exceptions;

public class ExceptionAssertionSpecs
public class AggregateExceptionSpecs
{
[Fact]
public void When_method_throws_an_empty_AggregateException_it_should_fail()
Expand Down
65 changes: 65 additions & 0 deletions Tests/FluentAssertions.Specs/Exceptions/ExceptionMessageSpecs.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
using System;
using System.Threading.Tasks;
using FluentAssertions.Execution;
using Xunit;
using Xunit.Sdk;

namespace FluentAssertions.Specs.Exceptions;

public class ExceptionMessageSpecs
{
[Fact]
public void Can_assert_the_exception_message_does_not_include_a_specific_string()
{
// Arrange
Action throwException = () => throw new InvalidOperationException("Something bad happened");

// Act
Action act = () => throwException.Should()
.Throw<InvalidOperationException>()
.WithoutMessage("*bad*")
.Which.Should().Be(typeof(InvalidOperationException));

// Assert
act.Should().Throw<XunitException>()
.WithMessage(
"Did not expect the exception message to match the equivalent of \"*bad*\", but \"Something bad happened\" matches.");
}

[Fact]
public void Only_check_the_message_of_an_actual_exception()
{
// Arrange
Action dontThrowAtAll = () => { };

// Act
var act = () =>
{
using var _ = new AssertionScope();

return dontThrowAtAll.Should()
.Throw<InvalidOperationException>()
.WithoutMessage("*bad*");
};

// Assert
act.Should().Throw<XunitException>()
.WithMessage(
"Expected*System.InvalidOperationException*no exception*");
}

[Fact]
public async Task Can_assert_an_async_exception_message_does_not_include_a_specific_string()
{
// Arrange
Func<Task> throwsAsync = () => throw new AggregateException(new ArgumentException("That was wrong."));

// Act
var act = () => throwsAsync.Should().ThrowAsync<ArgumentException>().WithoutMessage("That was wrong.");

// Assert
await act.Should().ThrowAsync<XunitException>()
.WithMessage(
"Did not expect the exception message to match the equivalent of \"*wrong*\", but \"That was wrong.\" matches.");
Comment thread
dennisdoomen marked this conversation as resolved.
}
}
20 changes: 10 additions & 10 deletions Tests/FluentAssertions.Specs/Exceptions/OuterExceptionSpecs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public void When_subject_throws_expected_exception_but_with_unexpected_message_i
{
// Assert
ex.Message.Should().Match(
"Expected exception message to match the equivalent of*\"some message\", but*\"some\" does not*");
"Expected the exception message to match the equivalent of*\"some message\", but*\"some\" does not*");
}
}

Expand All @@ -62,7 +62,7 @@ public void Long_exception_messages_are_rendered_over_multiple_lines()
// Assert
ex.Message.Should().Match(
"""
Expected exception message to match the equivalent of
Expected the exception message to match the equivalent of

"*",

Expand Down Expand Up @@ -100,7 +100,7 @@ public void Multiline_exception_messages_are_rendered_over_multiple_lines()
// Assert
ex.Message.Should().Match(
"""
Expected exception message to match the equivalent of
Expected the exception message to match the equivalent of

"line1*
line2",
Expand Down Expand Up @@ -135,7 +135,7 @@ public void Short_exception_messages_are_rendered_on_a_single_line()
{
// Assert
ex.Message.Should().Match(
"""Expected exception message to match the equivalent of "*", but "some" does not.""");
"""Expected the exception message to match the equivalent of "*", but "some" does not.""");
}
}

Expand Down Expand Up @@ -168,7 +168,7 @@ public void When_subject_throws_expected_exception_with_message_that_does_not_st
// Assert
action.Should().Throw<Exception>()
.WithMessage(
"Expected exception message to match the equivalent of*\"Expected mes*\", but*\"OxpectOd message\" does not*");
"Expected the exception message to match the equivalent of*\"Expected mes*\", but*\"OxpectOd message\" does not*");
}

[Fact]
Expand Down Expand Up @@ -201,7 +201,7 @@ public void When_subject_throws_expected_exception_with_message_that_does_not_st
// Assert
action.Should().Throw<Exception>()
.WithMessage(
"Expected exception message to match the equivalent of*\"expected mes*\", but*\"OxpectOd message\" does not*");
"Expected the exception message to match the equivalent of*\"expected mes*\", but*\"OxpectOd message\" does not*");
}

[Fact]
Expand All @@ -224,7 +224,7 @@ public void When_subject_throws_some_exception_with_unexpected_message_it_should
{
// Assert
ex.Message.Should().Match(
"Expected exception message to match the equivalent of \"message2\" because we want to test the failure message, but \"message1\" does not*");
"Expected the exception message to match the equivalent of \"message2\" because we want to test the failure message, but \"message1\" does not*");
}
}

Expand All @@ -248,7 +248,7 @@ public void When_subject_throws_some_exception_with_an_empty_message_it_should_t
{
// Assert
ex.Message.Should().Match(
"Expected exception message to match the equivalent of \"message2\"*, but \"\"*");
"Expected the exception message to match the equivalent of \"message2\"*, but \"\"*");
}
}

Expand All @@ -272,7 +272,7 @@ public void
{
// Assert
ex.Message.Should().Match(
"Expected exception message to match the equivalent of*\"message2\",*but*message2*someParam*");
"Expected the exception message to match the equivalent of*\"message2\",*but*message2*someParam*");
}
}

Expand Down Expand Up @@ -343,7 +343,7 @@ public void When_subject_throws_exception_with_message_with_braces_but_a_differe
{
// Assert
ex.Message.Should().Match(
"Expected exception message to match the equivalent of*\"message without\"*, but*\"message with {}*");
"Expected the exception message to match the equivalent of*\"message without\"*, but*\"message with {}*");
}
}

Expand Down
1 change: 1 addition & 0 deletions docs/_pages/releases.md
Loading