Skip to content

Commit

Permalink
Define lazy version of AssertionScope.FailWith (#921)
Browse files Browse the repository at this point in the history
  • Loading branch information
krajek authored and dennisdoomen committed Sep 25, 2018
1 parent c6e72d6 commit 2dc2217
Show file tree
Hide file tree
Showing 4 changed files with 138 additions and 36 deletions.
48 changes: 30 additions & 18 deletions Src/FluentAssertions/Execution/AssertionScope.cs
Expand Up @@ -154,7 +154,7 @@ public AssertionScope BecauseOf(string because, params object[] becauseArgs)
/// If an expectation was set through a prior call to <see cref="WithExpectation"/>, then the failure message is appended to that
/// expectation.
/// </remarks>
/// <param name="expectation">The format string that represents the failure message.</param>
/// <param name="message">The format string that represents the failure message.</param>
/// <param name="args">Optional arguments to any numbered placeholders.</param>
public AssertionScope WithExpectation(string message, params object[] args)
{
Expand Down Expand Up @@ -201,23 +201,10 @@ public AssertionScope ForCondition(bool condition)
/// <summary>
/// Sets the failure message when the assertion is not met, or completes the failure message set to a
/// prior call to <see cref="FluentAssertions.Execution.AssertionScope.WithExpectation"/>.
/// <paramref name="failReasonFunc"/> will not be called unless the assertion is not met.
/// </summary>
/// <remarks>
/// In addition to the numbered <see cref="string.Format(string,object[])"/>-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 <see cref="FluentAssertions.Execution.AssertionScope.BecauseOf"/>. Other named placeholders will be replaced with
/// the <see cref="FluentAssertions.Execution.AssertionScope.Current"/> scope data passed through
/// <see cref="FluentAssertions.Execution.AssertionScope.AddNonReportable"/> and
/// <see cref="FluentAssertions.Execution.AssertionScope.AddReportable"/>. 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 <see cref="AssertionScope"/> constructor.
/// Note that only 10 <paramref name="args"/> are supported in combination with a {reason}.
/// If an expectation was set through a prior call to <see cref="FluentAssertions.Execution.AssertionScope.WithExpectation"/>,
/// then the failure message is appended to that expectation.
/// </remarks>
/// <param name="message">The format string that represents the failure message.</param>
/// <param name="args">Optional arguments to any numbered placeholders.</param>
public Continuation FailWith(string message, params object[] args)
/// <param name="failReasonFunc">Function returning <see cref="FailReason"/> object on demand. Called only when the assertion is not met.</param>
public Continuation FailWith(Func<FailReason> failReasonFunc)
{
try
{
Expand All @@ -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)
{
Expand All @@ -244,6 +232,30 @@ public Continuation FailWith(string message, params object[] args)
}
}

/// <summary>
/// Sets the failure message when the assertion is not met, or completes the failure message set to a
/// prior call to <see cref="FluentAssertions.Execution.AssertionScope.WithExpectation"/>.
/// </summary>
/// <remarks>
/// In addition to the numbered <see cref="string.Format(string,object[])"/>-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 <see cref="FluentAssertions.Execution.AssertionScope.BecauseOf"/>. Other named placeholders will be replaced with
/// the <see cref="FluentAssertions.Execution.AssertionScope.Current"/> scope data passed through
/// <see cref="FluentAssertions.Execution.AssertionScope.AddNonReportable"/> and
/// <see cref="FluentAssertions.Execution.AssertionScope.AddReportable"/>. 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 <see cref="AssertionScope"/> constructor.
/// Note that only 10 <paramref name="args"/> are supported in combination with a {reason}.
/// If an expectation was set through a prior call to <see cref="FluentAssertions.Execution.AssertionScope.WithExpectation"/>,
/// then the failure message is appended to that expectation.
/// </remarks>
/// <param name="message">The format string that represents the failure message.</param>
/// <param name="args">Optional arguments to any numbered placeholders.</param>
public Continuation FailWith(string message, params object[] args)
{
return FailWith(() => new FailReason(message, args));
}

private string GetIdentifier()
{
if (!string.IsNullOrEmpty(Context))
Expand Down
39 changes: 39 additions & 0 deletions Src/FluentAssertions/Execution/FailReason.cs
@@ -0,0 +1,39 @@
namespace FluentAssertions.Execution
{
/// <summary>
/// Represents assertion fail reason. Contains the message and arguments for
/// message's numbered placeholders.
/// </summary>
/// <remarks>
/// In addition to the numbered <see cref="string.Format(string,object[])"/>-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 <see cref="FluentAssertions.Execution.AssertionScope.BecauseOf"/>. Other named placeholders will be replaced with
/// the <see cref="FluentAssertions.Execution.AssertionScope.Current"/> scope data passed through
/// <see cref="FluentAssertions.Execution.AssertionScope.AddNonReportable"/> and
/// <see cref="FluentAssertions.Execution.AssertionScope.AddReportable"/>. 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 <see cref="AssertionScope"/> constructor.
/// Note that only 10 arguments are supported in combination with a {reason}.
/// </remarks>
public class FailReason
{
public FailReason(string message, params object[] args)
{
Message = message;
Args = args;
}

/// <summary>
/// Message to be displayed in case of failed assertion. May contain
/// numbered <see cref="string.Format(string,object[])"/>-style placeholders as well
/// as specialized placeholders.
/// </summary>
public string Message { get; }

/// <summary>
/// Arguments for the numbered <see cref="string.Format(string,object[])"/>-style placeholders
/// of <see cref="Message"/>.
/// </summary>
public object[] Args { get; }
}
}
31 changes: 13 additions & 18 deletions Src/FluentAssertions/Primitives/StringEqualityValidator.cs
@@ -1,5 +1,6 @@
using System;
using FluentAssertions.Common;
using FluentAssertions.Execution;

namespace FluentAssertions.Primitives
{
Expand Down Expand Up @@ -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
Expand Down
56 changes: 56 additions & 0 deletions Tests/Shared.Specs/AssertionScopeSpecs.cs
Expand Up @@ -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()
{
Expand Down

0 comments on commit 2dc2217

Please sign in to comment.