diff --git a/Src/FluentAssertions/Execution/AssertionScope.cs b/Src/FluentAssertions/Execution/AssertionScope.cs
index 6c5d11a782..fdb1d8a652 100644
--- a/Src/FluentAssertions/Execution/AssertionScope.cs
+++ b/Src/FluentAssertions/Execution/AssertionScope.cs
@@ -154,7 +154,7 @@ public AssertionScope BecauseOf(string because, params object[] becauseArgs)
/// If an expectation was set through a prior call to , then the failure message is appended to that
/// expectation.
///
- /// The format string that represents the failure message.
+ /// The format string that represents the failure message.
/// Optional arguments to any numbered placeholders.
public AssertionScope WithExpectation(string message, params object[] args)
{
@@ -201,23 +201,10 @@ public AssertionScope ForCondition(bool condition)
///
/// Sets the failure message when the assertion is not met, or completes the failure message set to a
/// prior call to .
+ /// will not be called unless the assertion is not met.
///
- ///
- /// In addition to the numbered -style placeholders, messages may contain a few
- /// specialized placeholders as well. For instance, {reason} will be replaced with the reason of the assertion as passed
- /// to . Other named placeholders will be replaced with
- /// the scope data passed through
- /// and
- /// . Finally, a description of the
- /// current subject can be passed through the {context:description} placeholder. This is used in the message if no
- /// explicit context is specified through the constructor.
- /// Note that only 10 are supported in combination with a {reason}.
- /// If an expectation was set through a prior call to ,
- /// then the failure message is appended to that expectation.
- ///
- /// The format string that represents the failure message.
- /// Optional arguments to any numbered placeholders.
- public Continuation FailWith(string message, params object[] args)
+ /// Function returning object on demand. Called only when the assertion is not met.
+ public Continuation FailWith(Func failReasonFunc)
{
try
{
@@ -226,7 +213,8 @@ public Continuation FailWith(string message, params object[] args)
string localReason = reason != null ? reason() : "";
var messageBuilder = new MessageBuilder(useLineBreaks);
string identifier = GetIdentifier();
- string result = messageBuilder.Build(message, args, localReason, contextData, identifier, fallbackIdentifier);
+ var failReason = failReasonFunc();
+ string result = messageBuilder.Build(failReason.Message, failReason.Args, localReason, contextData, identifier, fallbackIdentifier);
if (expectation != null)
{
@@ -244,6 +232,30 @@ public Continuation FailWith(string message, params object[] args)
}
}
+ ///
+ /// Sets the failure message when the assertion is not met, or completes the failure message set to a
+ /// prior call to .
+ ///
+ ///
+ /// In addition to the numbered -style placeholders, messages may contain a few
+ /// specialized placeholders as well. For instance, {reason} will be replaced with the reason of the assertion as passed
+ /// to . Other named placeholders will be replaced with
+ /// the scope data passed through
+ /// and
+ /// . Finally, a description of the
+ /// current subject can be passed through the {context:description} placeholder. This is used in the message if no
+ /// explicit context is specified through the constructor.
+ /// Note that only 10 are supported in combination with a {reason}.
+ /// If an expectation was set through a prior call to ,
+ /// then the failure message is appended to that expectation.
+ ///
+ /// The format string that represents the failure message.
+ /// Optional arguments to any numbered placeholders.
+ public Continuation FailWith(string message, params object[] args)
+ {
+ return FailWith(() => new FailReason(message, args));
+ }
+
private string GetIdentifier()
{
if (!string.IsNullOrEmpty(Context))
diff --git a/Src/FluentAssertions/Execution/FailReason.cs b/Src/FluentAssertions/Execution/FailReason.cs
new file mode 100644
index 0000000000..605d2834ab
--- /dev/null
+++ b/Src/FluentAssertions/Execution/FailReason.cs
@@ -0,0 +1,39 @@
+namespace FluentAssertions.Execution
+{
+ ///
+ /// Represents assertion fail reason. Contains the message and arguments for
+ /// message's numbered placeholders.
+ ///
+ ///
+ /// In addition to the numbered -style placeholders, messages may contain a few
+ /// specialized placeholders as well. For instance, {reason} will be replaced with the reason of the assertion as passed
+ /// to . Other named placeholders will be replaced with
+ /// the scope data passed through
+ /// and
+ /// . Finally, a description of the
+ /// current subject can be passed through the {context:description} placeholder. This is used in the message if no
+ /// explicit context is specified through the constructor.
+ /// Note that only 10 arguments are supported in combination with a {reason}.
+ ///
+ public class FailReason
+ {
+ public FailReason(string message, params object[] args)
+ {
+ Message = message;
+ Args = args;
+ }
+
+ ///
+ /// Message to be displayed in case of failed assertion. May contain
+ /// numbered -style placeholders as well
+ /// as specialized placeholders.
+ ///
+ public string Message { get; }
+
+ ///
+ /// Arguments for the numbered -style placeholders
+ /// of .
+ ///
+ public object[] Args { get; }
+ }
+}
diff --git a/Src/FluentAssertions/Primitives/StringEqualityValidator.cs b/Src/FluentAssertions/Primitives/StringEqualityValidator.cs
index 9bfc29ebc3..720c983ab0 100644
--- a/Src/FluentAssertions/Primitives/StringEqualityValidator.cs
+++ b/Src/FluentAssertions/Primitives/StringEqualityValidator.cs
@@ -1,5 +1,6 @@
using System;
using FluentAssertions.Common;
+using FluentAssertions.Execution;
namespace FluentAssertions.Primitives
{
@@ -27,29 +28,23 @@ protected override bool ValidateAgainstSuperfluousWhitespace()
protected override bool ValidateAgainstLengthDifferences()
{
- // Logic is a little bit convoluted because I want to avoid calculation
- // of mismatch segment in case of equalLength == true for performance reason.
- // If lazy version of FailWith would be introduced, calculation of mismatch
- // segment can be moved directly to FailWith's argument
- bool equalLength = subject.Length == expected.Length;
+ return assertion
+ .ForCondition(subject.Length == expected.Length)
+ .FailWith(() =>
+ {
+ string mismatchSegment = GetMismatchSegmentForStringsOfDifferentLengths();
- string mismatchSegment = GetMismatchSegmentForStringsOfDifferentLengths(equalLength);
+ string message = ExpectationDescription +
+ "{0} with a length of {1}{reason}, but {2} has a length of {3}, differs near " + mismatchSegment + ".";
- return assertion
- .ForCondition(equalLength)
- .FailWith(
- ExpectationDescription + "{0} with a length of {1}{reason}, but {2} has a length of {3}, differs near " + mismatchSegment + ".",
- expected, expected.Length, subject, subject.Length)
- .SourceSucceeded;
+ return new FailReason(message, expected, expected.Length, subject, subject.Length);
+
+ }
+ ).SourceSucceeded; ;
}
- private string GetMismatchSegmentForStringsOfDifferentLengths(bool equalLength)
+ private string GetMismatchSegmentForStringsOfDifferentLengths()
{
- if (equalLength)
- {
- return "";
- }
-
int indexOfMismatch = subject.IndexOfFirstMismatch(expected, comparisonMode);
// If there is no difference it means that either
diff --git a/Tests/Shared.Specs/AssertionScopeSpecs.cs b/Tests/Shared.Specs/AssertionScopeSpecs.cs
index 20beb29428..e10f175b79 100644
--- a/Tests/Shared.Specs/AssertionScopeSpecs.cs
+++ b/Tests/Shared.Specs/AssertionScopeSpecs.cs
@@ -103,6 +103,62 @@ public void When_disposed_it_should_throw_any_failures_and_properly_format_using
}
}
+ [Fact]
+ public void When_lazy_version_is_not_disposed_it_should_not_execute_fail_reason_function()
+ {
+ //-----------------------------------------------------------------------------------------------------------
+ // Arrange
+ //-----------------------------------------------------------------------------------------------------------
+ var scope = new AssertionScope();
+ bool failReasonCalled = false;
+ AssertionScope.Current
+ .ForCondition(true)
+ .FailWith(() =>
+ {
+ failReasonCalled = true;
+ return new FailReason("Failure");
+ });
+
+ //-----------------------------------------------------------------------------------------------------------
+ // Act
+ //-----------------------------------------------------------------------------------------------------------
+ Action act = scope.Dispose;
+
+ //-----------------------------------------------------------------------------------------------------------
+ // Assert
+ //-----------------------------------------------------------------------------------------------------------
+ act();
+ failReasonCalled.Should().BeFalse(" fail reason function cannot be called for scope that successful");
+ }
+
+ [Fact]
+ public void When_lazy_version_is_disposed_it_should_throw_any_failures_and_properly_format_using_args()
+ {
+ //-----------------------------------------------------------------------------------------------------------
+ // Arrange
+ //-----------------------------------------------------------------------------------------------------------
+ var scope = new AssertionScope();
+
+ AssertionScope.Current.FailWith(() => new FailReason("Failure{0}", 1));
+
+ //-----------------------------------------------------------------------------------------------------------
+ // Act
+ //-----------------------------------------------------------------------------------------------------------
+ Action act = scope.Dispose;
+
+ //-----------------------------------------------------------------------------------------------------------
+ // Assert
+ //-----------------------------------------------------------------------------------------------------------
+ try
+ {
+ act();
+ }
+ catch (Exception exception)
+ {
+ exception.Message.Should().StartWith("Failure1");
+ }
+ }
+
[Fact]
public void When_multiple_scopes_are_nested_it_should_throw_all_failures_from_the_outer_scope()
{