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

Feature: Add collections methods BeProperSubsetOf / BeProperSupersetOf / BeSupersetOf #2432

Open
wants to merge 3 commits into
base: develop
Choose a base branch
from
Open
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
135 changes: 116 additions & 19 deletions Src/FluentAssertions/Collections/GenericCollectionAssertions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -653,6 +653,7 @@ public AndConstraint<TAssertions> BeNullOrEmpty(string because = "", params obje

/// <summary>
/// Asserts that the collection is a subset of the <paramref name="expectedSuperset" />.
/// If <paramref name="expectedSuperset" /> is equivalent to the subject then its still a valid superset since it contains all of the elements from the subject.
/// </summary>
/// <param name="expectedSuperset">An <see cref="IEnumerable{T}"/> with the expected superset.</param>
/// <param name="because">
Expand All @@ -666,21 +667,95 @@ public AndConstraint<TAssertions> BeNullOrEmpty(string because = "", params obje
public AndConstraint<TAssertions> BeSubsetOf(IEnumerable<T> expectedSuperset, string because = "",
params object[] becauseArgs)
{
Guard.ThrowIfArgumentIsNull(expectedSuperset, nameof(expectedSuperset),
"Cannot verify a subset against a <null> collection.");
Guard.ThrowIfArgumentIsNull(expectedSuperset, nameof(expectedSuperset), "Cannot verify a subset against a <null> collection.");
AssertBeSubsetOf(expectedSuperset.ConvertOrCastToSet(), "subset", because, becauseArgs);

Execute.Assertion
.BecauseOf(because, becauseArgs)
.WithExpectation("Expected {context:collection} to be a subset of {0}{reason}, ", expectedSuperset)
.Given(() => Subject)
.ForCondition(subject => subject is not null)
.FailWith("but found <null>.")
.Then
.Given(subject => subject.Except(expectedSuperset))
.ForCondition(excessItems => !excessItems.Any())
.FailWith("but items {0} are not part of the superset.", excessItems => excessItems)
.Then
.ClearExpectation();
return new AndConstraint<TAssertions>((TAssertions)this);
}

/// <summary>
/// Asserts that the collection is a superset of the <paramref name="expectedSubset" />.
Meir017 marked this conversation as resolved.
Show resolved Hide resolved
/// If <paramref name="expectedSubset" /> is equivalent to the subject then it's still a valid subset since all of it's items are contained in the subject.
/// </summary>
/// <param name="expectedSubset">An <see cref="IEnumerable{T}"/> with the expected subset.</param>
/// <param name="because">
/// A 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 <paramref name="because" />.
/// </param>
/// <exception cref="ArgumentNullException"><paramref name="expectedSubset"/> is <see langword="null"/>.</exception>
/// <remarks>This method is an alias for <see cref="Contain(IEnumerable{T}, string, object[])"/></remarks>
public AndConstraint<TAssertions> BeSupersetOf(IEnumerable<T> expectedSubset, string because = "",
params object[] becauseArgs)
{
AssertContainment(expectedSubset, "be a superset of", because, becauseArgs);

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

/// <summary>
/// Asserts that the collection is a proper subset of the <paramref name="expectedProperSuperset" />.
/// If <paramref name="expectedProperSuperset" /> is equivalent to the subject then the subject is not a proper subset.
/// <paramref name="expectedProperSuperset" /> must contain at least one element in that is not present in the subject.
/// </summary>
/// <param name="expectedProperSuperset">An <see cref="IEnumerable{T}"/> with the expected superset.</param>
/// <param name="because">
/// A 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 <paramref name="because" />.
/// </param>
/// <exception cref="ArgumentNullException"><paramref name="expectedProperSuperset"/> is <see langword="null"/>.</exception>
public AndConstraint<TAssertions> BeProperSubsetOf(IEnumerable<T> expectedProperSuperset, string because = "",
params object[] becauseArgs)
{
Guard.ThrowIfArgumentIsNull(expectedProperSuperset, nameof(expectedProperSuperset), "Cannot verify a proper subset against a <null> collection.");
ISet<T> expectedItems = expectedProperSuperset.ConvertOrCastToSet();
AssertBeSubsetOf(expectedItems, "proper subset", because, becauseArgs);

if (expectedItems.Intersect(Subject).Count() == expectedItems.Count)
{
Execute.Assertion
.BecauseOf(because, becauseArgs)
.FailWith("Expected {context:collection} to be a proper subset of {0}{reason}, but items {1} are equivalent to the superset {2}",
expectedItems, Subject, expectedProperSuperset);
}

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

/// <summary>
/// Asserts that the collection is a proper subset of the <paramref name="expectedProperSubset" />.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// Asserts that the collection is a proper subset of the <paramref name="expectedProperSubset" />.
/// Asserts that the collection is a proper superset of the <paramref name="expectedProperSubset" />.

This comment was marked as resolved.

This comment was marked as resolved.

This comment was marked as resolved.

/// If <paramref name="expectedProperSubset" /> is equivalent to the subject then the subject is not a proper superset.
/// The subject must contain at least one element in that is not present in <paramref name="expectedProperSubset" />.
/// </summary>
/// <param name="expectedProperSubset">An <see cref="IEnumerable{T}"/> with the expected subset.</param>
/// <param name="because">
/// A 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 <paramref name="because" />.
/// </param>
/// <exception cref="ArgumentNullException"><paramref name="expectedProperSubset"/> is <see langword="null"/>.</exception>
public AndConstraint<TAssertions> BeProperSupersetOf(IEnumerable<T> expectedProperSubset, string because = "",
params object[] becauseArgs)
{
ICollection<T> expectedProperSubsetItems = expectedProperSubset.ConvertOrCastToCollection();
AssertContainment(expectedProperSubsetItems, "be a proper superset of", because, becauseArgs);

ISet<T> actualItems = Subject.ConvertOrCastToSet();

if (actualItems.Intersect(expectedProperSubsetItems).Count() == actualItems.Count)
{
Execute.Assertion
.BecauseOf(because, becauseArgs)
.FailWith("Expected {context:collection} to be a proper superset of {0}{reason}, but items {1} are equivalent to the superset",
actualItems, expectedProperSubsetItems);
}

return new AndConstraint<TAssertions>((TAssertions)this);
}
Expand Down Expand Up @@ -774,6 +849,13 @@ public AndConstraint<TAssertions> BeNullOrEmpty(string because = "", params obje
/// <exception cref="ArgumentNullException"><paramref name="expected"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException"><paramref name="expected"/> is empty.</exception>
public AndConstraint<TAssertions> Contain(IEnumerable<T> expected, string because = "", params object[] becauseArgs)
{
AssertContainment(expected, "contain", because, becauseArgs);

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

private void AssertContainment(IEnumerable<T> expected, string containmentType, string because = "", params object[] becauseArgs)
{
Guard.ThrowIfArgumentIsNull(expected, nameof(expected), "Cannot verify containment against a <null> collection");

Expand All @@ -783,7 +865,7 @@ public AndConstraint<TAssertions> Contain(IEnumerable<T> expected, string becaus
bool success = Execute.Assertion
.BecauseOf(because, becauseArgs)
.ForCondition(Subject is not null)
.FailWith("Expected {context:collection} to contain {0}{reason}, but found <null>.", expectedObjects);
.FailWith("Expected {context:collection} to " + containmentType + " {0}{reason}, but found <null>.", expectedObjects);

if (success)
{
Expand All @@ -795,20 +877,18 @@ public AndConstraint<TAssertions> Contain(IEnumerable<T> expected, string becaus
{
Execute.Assertion
.BecauseOf(because, becauseArgs)
.FailWith("Expected {context:collection} {0} to contain {1}{reason}, but could not find {2}.",
.FailWith("Expected {context:collection} {0} to " + containmentType + " {1}{reason}, but could not find {2}.",
Subject, expectedObjects, missingItems);
}
else
{
Execute.Assertion
.BecauseOf(because, becauseArgs)
.FailWith("Expected {context:collection} {0} to contain {1}{reason}.",
.FailWith("Expected {context:collection} {0} to " + containmentType + " {1}{reason}.",
Subject, expectedObjects.Single());
}
}
}

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

/// <summary>
Expand Down Expand Up @@ -3443,6 +3523,23 @@ private static T SuccessorOf(T predecessor, TCollection subject)
return index < (collection.Count - 1) ? collection[index + 1] : default;
}

private void AssertBeSubsetOf(ISet<T> expectedSuperset, string subsetType, string because = "",
params object[] becauseArgs)
{
Execute.Assertion
.BecauseOf(because, becauseArgs)
.WithExpectation("Expected {context:collection} to be a " + subsetType + " of {0}{reason}, ", expectedSuperset)
.Given(() => Subject)
.ForCondition(subject => subject is not null)
.FailWith("but found <null>.")
.Then
.Given(subject => subject.Except(expectedSuperset))
.ForCondition(excessItems => !excessItems.Any())
.FailWith("but items {0} are not part of the superset.", excessItems => excessItems)
.Then
.ClearExpectation();
}

private string[] CollectFailuresFromInspectors(IEnumerable<Action<T>> elementInspectors)
{
string[] collectionFailures;
Expand Down
5 changes: 5 additions & 0 deletions Src/FluentAssertions/Common/EnumerableExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ public static IList<T> ConvertOrCastToList<T>(this IEnumerable<T> source)
return source as IList<T> ?? source.ToList();
}

public static ISet<T> ConvertOrCastToSet<T>(this IEnumerable<T> source)
{
return source as ISet<T> ?? new HashSet<T>(source);
}

/// <summary>
/// Searches for the first different element in two sequences using specified <paramref name="equalityComparison" />
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -413,7 +413,10 @@ namespace FluentAssertions.Collections
public FluentAssertions.AndConstraint<FluentAssertions.Collections.SubsequentOrderingAssertions<T>> BeInDescendingOrder<TSelector>(System.Linq.Expressions.Expression<System.Func<T, TSelector>> propertyExpression, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<FluentAssertions.Collections.SubsequentOrderingAssertions<T>> BeInDescendingOrder<TSelector>(System.Linq.Expressions.Expression<System.Func<T, TSelector>> propertyExpression, System.Collections.Generic.IComparer<TSelector> comparer, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> BeNullOrEmpty(string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> BeProperSubsetOf(System.Collections.Generic.IEnumerable<T> expectedProperSuperset, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> BeProperSupersetOf(System.Collections.Generic.IEnumerable<T> expectedProperSubset, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> BeSubsetOf(System.Collections.Generic.IEnumerable<T> expectedSuperset, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> BeSupersetOf(System.Collections.Generic.IEnumerable<T> expectedSubset, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> Contain(System.Collections.Generic.IEnumerable<T> expected, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndWhichConstraint<TAssertions, T> Contain(System.Linq.Expressions.Expression<System.Func<T, bool>> predicate, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndWhichConstraint<TAssertions, T> Contain(T expected, string because = "", params object[] becauseArgs) { }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -426,7 +426,10 @@ namespace FluentAssertions.Collections
public FluentAssertions.AndConstraint<FluentAssertions.Collections.SubsequentOrderingAssertions<T>> BeInDescendingOrder<TSelector>(System.Linq.Expressions.Expression<System.Func<T, TSelector>> propertyExpression, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<FluentAssertions.Collections.SubsequentOrderingAssertions<T>> BeInDescendingOrder<TSelector>(System.Linq.Expressions.Expression<System.Func<T, TSelector>> propertyExpression, System.Collections.Generic.IComparer<TSelector> comparer, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> BeNullOrEmpty(string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> BeProperSubsetOf(System.Collections.Generic.IEnumerable<T> expectedProperSuperset, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> BeProperSupersetOf(System.Collections.Generic.IEnumerable<T> expectedProperSubset, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> BeSubsetOf(System.Collections.Generic.IEnumerable<T> expectedSuperset, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> BeSupersetOf(System.Collections.Generic.IEnumerable<T> expectedSubset, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> Contain(System.Collections.Generic.IEnumerable<T> expected, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndWhichConstraint<TAssertions, T> Contain(System.Linq.Expressions.Expression<System.Func<T, bool>> predicate, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndWhichConstraint<TAssertions, T> Contain(T expected, string because = "", params object[] becauseArgs) { }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -405,7 +405,10 @@ namespace FluentAssertions.Collections
public FluentAssertions.AndConstraint<FluentAssertions.Collections.SubsequentOrderingAssertions<T>> BeInDescendingOrder<TSelector>(System.Linq.Expressions.Expression<System.Func<T, TSelector>> propertyExpression, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<FluentAssertions.Collections.SubsequentOrderingAssertions<T>> BeInDescendingOrder<TSelector>(System.Linq.Expressions.Expression<System.Func<T, TSelector>> propertyExpression, System.Collections.Generic.IComparer<TSelector> comparer, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> BeNullOrEmpty(string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> BeProperSubsetOf(System.Collections.Generic.IEnumerable<T> expectedProperSuperset, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> BeProperSupersetOf(System.Collections.Generic.IEnumerable<T> expectedProperSubset, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> BeSubsetOf(System.Collections.Generic.IEnumerable<T> expectedSuperset, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> BeSupersetOf(System.Collections.Generic.IEnumerable<T> expectedSubset, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> Contain(System.Collections.Generic.IEnumerable<T> expected, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndWhichConstraint<TAssertions, T> Contain(System.Linq.Expressions.Expression<System.Func<T, bool>> predicate, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndWhichConstraint<TAssertions, T> Contain(T expected, string because = "", params object[] becauseArgs) { }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -413,7 +413,10 @@ namespace FluentAssertions.Collections
public FluentAssertions.AndConstraint<FluentAssertions.Collections.SubsequentOrderingAssertions<T>> BeInDescendingOrder<TSelector>(System.Linq.Expressions.Expression<System.Func<T, TSelector>> propertyExpression, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<FluentAssertions.Collections.SubsequentOrderingAssertions<T>> BeInDescendingOrder<TSelector>(System.Linq.Expressions.Expression<System.Func<T, TSelector>> propertyExpression, System.Collections.Generic.IComparer<TSelector> comparer, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> BeNullOrEmpty(string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> BeProperSubsetOf(System.Collections.Generic.IEnumerable<T> expectedProperSuperset, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> BeProperSupersetOf(System.Collections.Generic.IEnumerable<T> expectedProperSubset, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> BeSubsetOf(System.Collections.Generic.IEnumerable<T> expectedSuperset, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> BeSupersetOf(System.Collections.Generic.IEnumerable<T> expectedSubset, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> Contain(System.Collections.Generic.IEnumerable<T> expected, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndWhichConstraint<TAssertions, T> Contain(System.Linq.Expressions.Expression<System.Func<T, bool>> predicate, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndWhichConstraint<TAssertions, T> Contain(T expected, string because = "", params object[] becauseArgs) { }
Expand Down