Skip to main content

Equality and containment

Describes how to compare a collection with another collection, and how to verify that it contains, starts with or ends with items.

ExpectationNegatedSummary
IsEqualToIsNotEqualTohas the same items as the expected collection
Contains(item)DoesNotContaincontains the item, optionally a number of times
Contains(predicate)DoesNotContaincontains an item that satisfies the predicate
Contains(collection)DoesNotContaincontains the expected items (a subset)
IsContainedInIsNotContainedInall its items are contained in the expected collection
StartsWithDoesNotStartWithstarts with the expected items
EndsWithDoesNotEndWithends with the expected items

Equality​

You can verify that a collection is equal to another collection:

IEnumerable<int> trackNumbers = Enumerable.Range(1, 3);

await That(trackNumbers).IsEqualTo([1, 2, 3]);
await That(trackNumbers).IsEqualTo([3, 2, 1]).InAnyOrder();
await That(trackNumbers).IsEqualTo([1, 1, 2, 2, 3, 3]).IgnoringDuplicates();
await That(trackNumbers).IsEqualTo([3, 3, 2, 2, 1, 1]).InAnyOrder().IgnoringDuplicates();

await That(trackNumbers).IsNotEqualTo([2, 3]);
await That(trackNumbers).IsNotEqualTo([4, 3, 2, 1]).InAnyOrder();

Without InAnyOrder() the items are compared in the order in which the collection enumerates them, which is not defined for a set.

With IgnoringDuplicates(), only which items occur matters, not how often: every expected item has to be matched by an item of the collection, and every item of the collection has to match an expected item.

Multi-dimensional arrays

A multi-dimensional array is only equal to an array of the same rank with the same length in every dimension, also with InAnyOrder() or IgnoringDuplicates(), while the other expectations on this page only look at its items. A failed comparison with a collection names an item of such an array by its index in every dimension (e.g. [1,2]). The same holds for equivalency, also when the collection order is ignored.

Contained items​

You can verify that the collection contains a specific item or not:

IEnumerable<string> songs = ["Come Together", "Something", "Let It Be"];

await That(songs).Contains("Let It Be");
await That(songs).DoesNotContain("Yesterday");

You can also set occurrence constraints on Contains:

IEnumerable<string> playlist = ["Let It Be", "Let It Be", "Let It Be", "Yesterday"];

await That(playlist).Contains("Let It Be").MoreThan(1);
await That(playlist).Contains("Let It Be").AtLeast(2);
await That(playlist).Contains("Let It Be").Exactly(3);
await That(playlist).Contains("Let It Be").AtMost(4);
await That(playlist).Contains("Let It Be").LessThan(5);
await That(playlist).Contains("Let It Be").Between(1).And(5);
await That(playlist).Contains("Yesterday").Once();
await That(playlist).Contains("Let It Be").AtLeast().Twice();
await That(playlist).Contains("Help!").Never();

AtLeast(2.Times()) with the Times() extension from aweXpect.Core reads the same, but the plain number is enough.

Predicate​

You can verify that the collection contains an item that satisfies a condition, with the same occurrence constraints:

IEnumerable<int> playCounts = [1, 1, 1, 2];

await That(playCounts).Contains(x => x > 1);
await That(playCounts).Contains(x => x == 1).Exactly(3);
await That(playCounts).DoesNotContain(x => x >= 42);

A quantifier such as Any().Satisfy(…) verifies the same and also accepts a nested expectation, and HasItem verifies the item at an index, see choosing an expectation.

Subset​

You can verify that a collection contains another collection as a subset:

IEnumerable<int> trackNumbers = Enumerable.Range(1, 3);

await That(trackNumbers).Contains([1, 2]);
await That(trackNumbers).Contains([3, 2]).InAnyOrder();
await That(trackNumbers).Contains([1, 1, 2, 2]).IgnoringDuplicates();
await That(trackNumbers).Contains([3, 3, 1, 1]).InAnyOrder().IgnoringDuplicates();
await That(trackNumbers).Contains([1, 3]).IgnoringInterspersedItems();

Without InAnyOrder the values must appear in the subject in the same order and contiguous, i.e. without other items in between, so [1, 3] is not contained in [1, 2, 3] unless IgnoringInterspersedItems is used.

IgnoringDuplicates() works as for equality, except that only the expected items have to be matched.

To check for a proper subset, append .Properly() (which would fail for equal collections).

InAnyOrder together with IgnoringInterspersedItems

InAnyOrder and IgnoringInterspersedItems exclude each other, here and for IsContainedIn: specifying the second one throws an InvalidOperationException.

Superset​

You can verify that a collection is contained in another collection (the other collection is a superset):

IEnumerable<int> trackNumbers = Enumerable.Range(1, 3);

await That(trackNumbers).IsContainedIn([1, 2, 3, 4]);
await That(trackNumbers).IsContainedIn([4, 3, 2, 1]).InAnyOrder();
await That(trackNumbers).IsContainedIn([1, 1, 2, 2, 3, 3, 4, 4]).IgnoringDuplicates();
await That(trackNumbers).IsContainedIn([1, 4, 2, 3]).IgnoringInterspersedItems();
await That(trackNumbers).IsNotContainedIn([1, 2]);

Without InAnyOrder the values must appear in the expected collection in the same order and contiguous, i.e. without other items in between, so [1, 3] is not contained in [1, 2, 3] unless IgnoringInterspersedItems is used.

IgnoringDuplicates() works as for equality, except that only the items of the collection have to match.

To check for a proper superset, append .Properly() (which would fail for equal collections).

Start / end​

You can verify that a collection starts or ends with other items or not:

IEnumerable<string> songs = ["Come Together", "Something", "Let It Be"];

await That(songs).StartsWith("Come Together", "Something");
await That(songs).EndsWith("Let It Be");
await That(songs).DoesNotStartWith("Let It Be");
await That(songs).DoesNotEndWith("Something");
note

EndsWith and DoesNotEndWith always enumerate the complete collection.

Predicates and expectations per item​

Instead of the expected items, IsEqualTo, Contains and IsContainedIn (and their negations) also accept one predicate or one expectation per item, with the same options as with expected items:

IEnumerable<string> songs = ["Come Together", "Something", "Let It Be"];

await That(songs).IsEqualTo([x => x.Length > 10, x => x == "Something", x => x.EndsWith("Be")]);
await That(songs).IsEqualTo([
x => x.IsEqualTo("Let It Be"),
x => x.StartsWith("Come"),
x => x.IsNotEmpty(),
]).InAnyOrder();
await That(songs).Contains([x => x == "Something", x => x.EndsWith("Be")]);

The failure message lists the predicates or expectations as the expected items:

using System.Linq.Expressions;

IEnumerable<string> songs = ["Two of Us", "Dig a Pony", "Let It Be"];
Expression<Func<string, bool>>[] expected =
[x => x == "Two of Us", x => x == "Dig a Pony", x => x == "Let It Be", x => x == "Get Back"];

await That(songs).IsEqualTo(expected);
Failure message
Expected that songs
is equal to collection expected in order,
but it lacked 1 of 4 expected items: x => (x == "Get Back")

Collection:
[
"Two of Us",
"Dig a Pony",
"Let It Be"
]

Expected:
[
x => (x == "Two of Us"),
x => (x == "Dig a Pony"),
x => (x == "Let It Be"),
x => (x == "Get Back")
]
Predicates with IgnoringDuplicates()

Predicates and expectations are never compared with each other. With IgnoringDuplicates(), one item may therefore match several of them, e.g. [5] is equal to [x => x > 0, x => x > 1] in any order ignoring duplicates.

Comparing items​

The expectations on this page compare the items with the expected items by their default equality, unless you specify a custom comparer, equivalency, a tolerance or, for strings, one of the string options:

IEnumerable<Album> albums = //...
Album expected = //...

await That(albums).Contains(expected).Equivalent();
await That(albums).StartsWith(expected).Using(new AlbumComparer());
await That(["COME TOGETHER", "SOMETHING"]).EndsWith(["something"]).IgnoringCase();

The rules for sets with a custom comparer apply as well.

Sets​

A set or a dictionary has no defined order

IsEqualTo, Contains with a collection and IsContainedIn compare the items in the order in which the collection enumerates them unless InAnyOrder() is used, so for a HashSet<T>, or for the entries, keys or values of a Dictionary<TKey, TValue>, the result depends on an implementation detail. IsEqualTo on a dictionary itself compares the entries by key instead. The analyzer rule aweXpect0006 warns about it and offers to append .InAnyOrder(). It also warns about StartsWith, EndsWith and IgnoringInterspersedItems(), which have no meaning for such a collection. Sorted sets and dictionaries are not reported.

A set that was created with a custom comparer (a HashSet<T>, SortedSet<T>, ImmutableHashSet<T>, ImmutableSortedSet<T> or FrozenSet<T>) compares its items with that comparer. Any other collection, including a set with the default comparer, is compared with the default equality. The comparer of such a set decides in every expectation that compares its items: IsEqualTo, Contains (with an item or a subset), IsContainedIn, HasItem, StartsWith, EndsWith and All().AreEqualTo.

Only the comparer of the subject is used, not the one of an expected set. A custom comparer, equivalency, a tolerance or a string option such as IgnoringCase() takes precedence over the comparer of the set, and Using(EqualityComparer<T>.Default) forces the default equality:

HashSet<string> albums = new(StringComparer.OrdinalIgnoreCase) { "Help!", "Revolver" };

await That(albums).Contains("HELP!");
await That(albums).IsEqualTo(["REVOLVER", "HELP!"]).InAnyOrder();
await That(albums).IsContainedIn(["REVOLVER", "ABBEY ROAD", "HELP!"]).InAnyOrder();
await That(albums).DoesNotContain("HELP!").Using(StringComparer.Ordinal);
await That(albums).IsNotEqualTo(["REVOLVER", "HELP!"]).InAnyOrder().Using(StringComparer.Ordinal);

Whenever the comparer of the set decides, the expectation names it, e.g. contains "HELP!" using the subject's StringComparer.OrdinalIgnoreCase at least once.

Untyped collections

For an untyped IEnumerable, only a set of the expected item type is recognized, e.g. a HashSet<string> for an expected string.

Tolerance​

The expectations that compare items with an expected value accept a tolerance for numbers such as int, long, double or decimal, for DateTime, DateTimeOffset and TimeSpan items, and on .NET 8 or later also for DateOnly and TimeOnly:

IEnumerable<double> durations = [1.01, 2.02, 3.04];
IEnumerable<int> playCounts = [9, 20, 31];

await That(durations).IsEqualTo([3.0, 2.0, 1.0]).Within(0.1).InAnyOrder();
await That(durations).Contains(2.0).Within(0.1);
await That(durations).Contains([2.0, 3.0]).Within(0.1);
await That(durations).IsContainedIn([1.0, 2.0, 3.0, 4.0]).Within(0.1);
await That(durations).StartsWith(1.0, 2.0).Within(0.1);
await That(durations).EndsWith(2.0, 3.0).Within(0.1);
await That(durations).HasItem(2.0).Within(0.1).AtIndex(1);
await That([2.04, 2.02, 2.01]).All().AreEqualTo(2.0).Within(0.1);
await That(playCounts).Contains(10).Within(1);

The values of a dictionary accept the same tolerance.

For double, float and decimal items, Percent() directly after Within interprets the tolerance as a percentage of each expected item:

await That([104.0, 209.0]).IsEqualTo([100.0, 200.0]).Within(5).Percent();

Without Within, the items of the time types use the default tolerance, if one is set.

DateOnly and TimeOnly tolerances

The tolerance for DateOnly items must be a whole number of days. TimeOnly items are compared on the clock face, so that 23:59 and 00:01 are two minutes apart.