Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support SatisfyRespectively for string collections #1201

Merged
merged 1 commit into from Dec 12, 2019
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
106 changes: 0 additions & 106 deletions Src/FluentAssertions/Collections/GenericCollectionAssertions.cs
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
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
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
}
}