Skip to main content

Collections

Describes the possible expectations for collections, e.g. arrays, lists, sets or any other IEnumerable<T>.

PageExpectations
Equality and containmentIsEqualTo, Contains, IsContainedIn, StartsWith, EndsWith
ItemsHasCount, IsEmpty, quantifiers like All() or None(), HasSingle, HasItem
Sort orderIsInAscendingOrder, IsInDescendingOrder
DictionariesContainsKey, ContainsValue, Keys, Values and dictionary equality
.NET 8 or later: IAsyncEnumerable<T>

On .NET 8 or later, every collection expectation, except the ones for dictionaries, works the same way for an IAsyncEnumerable<T>. The expectation enumerates it asynchronously and passes its cancellation token.

C# 13 or later

Some overloads rely on [OverloadResolutionPriority] to bind as described here, e.g. so that a string is expected as a single item and not as a sequence of characters, or that a params list or a collection expression […] is the expected collection. The attribute only takes effect with C# 13 or later, which is the default only for .NET 9 and later. With an older language version, such a call can bind to a different overload or fail with CS0121, so set <LangVersion> to 13 or latest in a project that targets an older framework.

Spans​

.NET 8 or later

Spans can only be passed to Expect.That on .NET 8 or later. On older targets, verify a property of the span, e.g. its Length, synchronously.

You can pass a Span<T> or a ReadOnlySpan<T> directly to Expect.That. The items are copied into a SpanWrapper<T>, which implements ICollection<T>, so all collection expectations are available:

await Expect.That("Help!".AsSpan()).HasCount(5);
await Expect.That("Help!".AsSpan()).Contains('!');

A span can't be kept across an await, so create it in the statement of the expectation.

Immutable arrays​

.NET 8 or later

Before .NET 8, only IsEmpty and IsNotEmpty are available for an ImmutableArray<T>. Call .AsEnumerable() on the array to use the other collection expectations.

A default ImmutableArray<T> is treated like a null collection, as the subject and as the expected collection. To tell the two apart, verify its IsDefault property.

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 Expect.That(albums).Contains("HELP!");
await Expect.That(albums).IsEqualTo(["REVOLVER", "HELP!"]).InAnyOrder();
await Expect.That(albums).IsContainedIn(["REVOLVER", "ABBEY ROAD", "HELP!"]).InAnyOrder();
await Expect.That(albums).DoesNotContain("HELP!").Using(StringComparer.Ordinal);
await Expect.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 recognised, 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, whose tolerance must be a whole number of days, and TimeOnly, whose items are compared on the clock face, so that 23:59 and 00:01 are two minutes apart:

IEnumerable<double> durations = [1.01, 2.02, 3.04];

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

A tolerance takes precedence over the comparer of a set. The values of a dictionary accept the same tolerance.

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