From 2dc221712b44f438d472d705d673e875730272bc Mon Sep 17 00:00:00 2001 From: Artur Krajewski Date: Tue, 25 Sep 2018 08:17:04 +0200 Subject: [PATCH] Define lazy version of `AssertionScope.FailWith` (#921) --- .../Execution/AssertionScope.cs | 48 ++++++++++------ Src/FluentAssertions/Execution/FailReason.cs | 39 +++++++++++++ .../Primitives/StringEqualityValidator.cs | 31 +++++----- Tests/Shared.Specs/AssertionScopeSpecs.cs | 56 +++++++++++++++++++ 4 files changed, 138 insertions(+), 36 deletions(-) create mode 100644 Src/FluentAssertions/Execution/FailReason.cs 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() {