Skip to content

Commit

Permalink
Merge pull request #1201 from jnyrup/SatisfyRespectively
Browse files Browse the repository at this point in the history
Support `SatisfyRespectively` for string collections
  • Loading branch information
jnyrup committed Dec 12, 2019
2 parents f2a5158 + 3acfc5e commit 1a87adb
Show file tree
Hide file tree
Showing 3 changed files with 143 additions and 106 deletions.
106 changes: 0 additions & 106 deletions Src/FluentAssertions/Collections/GenericCollectionAssertions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -568,112 +568,6 @@ private string GetExpressionOrderString<TSelector>(Expression<Func<T, TSelector>
BeEquivalentTo(repeatedExpectation, forceStringOrderingConfig, because, becauseArgs);
}

/// <summary>
/// Asserts that a collection contains exactly a given number of elements, which meet
/// the criteria provided by the element inspectors.
/// </summary>
/// <param name="elementInspectors">
/// The element inspectors, which inspect each element in turn. The
/// total number of element inspectors must exactly match the number of elements in the collection.
/// </param>
public AndConstraint<GenericCollectionAssertions<T>> SatisfyRespectively(params Action<T>[] elementInspectors)
{
return SatisfyRespectively(elementInspectors, string.Empty);
}

/// <summary>
/// Asserts that a collection contains exactly a given number of elements, which meet
/// the criteria provided by the element inspectors.
/// </summary>
/// <param name="expected">
/// The element inspectors, which inspect each element in turn. The
/// total number of element inspectors must exactly match the number of elements in the collection.
/// </param>
/// <param name="because">
/// An optional 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 <see cref="because" />.
/// </param>
public AndConstraint<GenericCollectionAssertions<T>> SatisfyRespectively(IEnumerable<Action<T>> expected, string because = "", params object[] becauseArgs)
{
Guard.ThrowIfArgumentIsNull(expected, nameof(expected), "Cannot verify against a <null> collection of inspectors");

ICollection<Action<T>> elementInspectors = expected.ConvertOrCastToCollection();
if (!elementInspectors.Any())
{
throw new ArgumentException("Cannot verify against an empty collection of inspectors", nameof(expected));
}

Execute.Assertion
.BecauseOf(because, becauseArgs)
.WithExpectation("Expected {context:collection} to satisfy all inspectors{reason}, ")
.ForCondition(!(Subject is null))
.FailWith("but collection is <null>.")
.Then
.ForCondition(Subject.Any())
.FailWith("but collection is empty.")
.Then
.ClearExpectation();

int elementsCount = Subject.Count();
int inspectorsCount = elementInspectors.Count;
Execute.Assertion
.BecauseOf(because, becauseArgs)
.ForCondition(elementsCount == inspectorsCount)
.FailWith("Expected {context:collection} to contain exactly {0} items{reason}, but it contains {1} items",
inspectorsCount, elementsCount);

string[] failuresFromInspectors = CollectFailuresFromInspectors(elementInspectors);

if (failuresFromInspectors.Any())
{
string failureMessage = Environment.NewLine
+ string.Join(Environment.NewLine, failuresFromInspectors.Select(x => x.IndentLines()));

Execute.Assertion
.BecauseOf(because, becauseArgs)
.WithExpectation("Expected {context:collection} to satisfy all inspectors{reason}, but some inspectors are not satisfied:")
.FailWithPreFormatted(failureMessage)
.Then
.ClearExpectation();
}

return new AndConstraint<GenericCollectionAssertions<T>>(this);
}

private string[] CollectFailuresFromInspectors(IEnumerable<Action<T>> elementInspectors)
{
string[] collectionFailures;
using (var collectionScope = new AssertionScope())
{
int index = 0;
foreach ((T element, Action<T> inspector) in Subject.Zip(elementInspectors, (element, inspector) => (element, inspector)))
{
string[] inspectorFailures;
using (var itemScope = new AssertionScope())
{
inspector(element);
inspectorFailures = itemScope.Discard();
}

if (inspectorFailures.Length > 0)
{
// Adding one tab and removing trailing dot to allow nested SatisfyRespectively
string failures = string.Join(Environment.NewLine, inspectorFailures.Select(x => x.IndentLines().TrimEnd('.')));
collectionScope.AddPreFormattedFailure($"At index {index}:{Environment.NewLine}{failures}");
}

index++;
}

collectionFailures = collectionScope.Discard();
}

return collectionFailures;
}

private static IEnumerable<TExpectation> RepeatAsManyAs<TExpectation>(TExpectation value, IEnumerable<T> enumerable)
{
if (enumerable is null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -669,5 +669,111 @@ public AndConstraint<TAssertions> NotContain(Expression<Func<T, bool>> predicate

return new AndWhichConstraint<TAssertions, T>((TAssertions)this, matchingElements);
}

/// <summary>
/// Asserts that a collection contains exactly a given number of elements, which meet
/// the criteria provided by the element inspectors.
/// </summary>
/// <param name="elementInspectors">
/// The element inspectors, which inspect each element in turn. The
/// total number of element inspectors must exactly match the number of elements in the collection.
/// </param>
public AndConstraint<TAssertions> SatisfyRespectively(params Action<T>[] elementInspectors)
{
return SatisfyRespectively(elementInspectors, string.Empty);
}

/// <summary>
/// Asserts that a collection contains exactly a given number of elements, which meet
/// the criteria provided by the element inspectors.
/// </summary>
/// <param name="expected">
/// The element inspectors, which inspect each element in turn. The
/// total number of element inspectors must exactly match the number of elements in the collection.
/// </param>
/// <param name="because">
/// An optional 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 <see cref="because" />.
/// </param>
public AndConstraint<TAssertions> SatisfyRespectively(IEnumerable<Action<T>> expected, string because = "", params object[] becauseArgs)
{
Guard.ThrowIfArgumentIsNull(expected, nameof(expected), "Cannot verify against a <null> collection of inspectors");

ICollection<Action<T>> elementInspectors = expected.ConvertOrCastToCollection();
if (!elementInspectors.Any())
{
throw new ArgumentException("Cannot verify against an empty collection of inspectors", nameof(expected));
}

Execute.Assertion
.BecauseOf(because, becauseArgs)
.WithExpectation("Expected {context:collection} to satisfy all inspectors{reason}, ")
.ForCondition(!(Subject is null))
.FailWith("but collection is <null>.")
.Then
.ForCondition(Subject.Any())
.FailWith("but collection is empty.")
.Then
.ClearExpectation();

int elementsCount = Subject.Count();
int inspectorsCount = elementInspectors.Count;
Execute.Assertion
.BecauseOf(because, becauseArgs)
.ForCondition(elementsCount == inspectorsCount)
.FailWith("Expected {context:collection} to contain exactly {0} items{reason}, but it contains {1} items",
inspectorsCount, elementsCount);

string[] failuresFromInspectors = CollectFailuresFromInspectors(elementInspectors);

if (failuresFromInspectors.Any())
{
string failureMessage = Environment.NewLine
+ string.Join(Environment.NewLine, failuresFromInspectors.Select(x => x.IndentLines()));

Execute.Assertion
.BecauseOf(because, becauseArgs)
.WithExpectation("Expected {context:collection} to satisfy all inspectors{reason}, but some inspectors are not satisfied:")
.FailWithPreFormatted(failureMessage)
.Then
.ClearExpectation();
}

return new AndConstraint<TAssertions>((TAssertions)this);
}

private string[] CollectFailuresFromInspectors(IEnumerable<Action<T>> elementInspectors)
{
string[] collectionFailures;
using (var collectionScope = new AssertionScope())
{
int index = 0;
foreach ((T element, Action<T> inspector) in Subject.Zip(elementInspectors, (element, inspector) => (element, inspector)))
{
string[] inspectorFailures;
using (var itemScope = new AssertionScope())
{
inspector(element);
inspectorFailures = itemScope.Discard();
}

if (inspectorFailures.Length > 0)
{
// Adding one tab and removing trailing dot to allow nested SatisfyRespectively
string failures = string.Join(Environment.NewLine, inspectorFailures.Select(x => x.IndentLines().TrimEnd('.')));
collectionScope.AddPreFormattedFailure($"At index {index}:{Environment.NewLine}{failures}");
}

index++;
}

collectionFailures = collectionScope.Discard();
}

return collectionFailures;
}
}
}
37 changes: 37 additions & 0 deletions Tests/Shared.Specs/GenericCollectionAssertionOfStringSpecs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1734,5 +1734,42 @@ public void When_asserting_collection_to_have_null_match_it_should_throw()
}

#endregion

#region SatisfyRespectively

[Fact]
public void When_string_collection_satisfies_all_inspectors_it_should_succeed()
{
// Arrange
string[] collection = new[] { "John", "Jane" };

// Act / Assert
collection.Should().SatisfyRespectively(
value => value.Should().Be("John"),
value => value.Should().Be("Jane")
);
}

[Fact]
public void When_string_collection_does_not_satisfy_all_inspectors_it_should_throw()
{
// Arrange
string[] collection = new[] { "Jack", "Jessica" };

// Act
Action act = () => collection.Should().SatisfyRespectively(new Action<string>[]
{
value => value.Should().Be("John"),
value => value.Should().Be("Jane")
}, "because we want to test the failure {0}", "message");

// Assert
act.Should().Throw<XunitException>().WithMessage(
"Expected collection to satisfy all inspectors because we want to test the failure message, but some inspectors are not satisfied"
+ "*John*Jack"
+ "*Jane*Jessica*");
}

#endregion
}
}

0 comments on commit 1a87adb

Please sign in to comment.