Skip to main content

Dictionaries

Describes the possible expectations for dictionaries.

ExpectationNegatedSummary
IsEqualToIsNotEqualTohas the same entries as the expected dictionary
ContainsDoesNotContaincontains the expected entry
ContainsKeyDoesNotContainKeycontains the expected key
ContainsKeysDoesNotContainKeyscontains all expected keys (negated: none of them)
ContainsValueDoesNotContainValuecontains the expected value
ContainsValuesDoesNotContainValuescontains all expected values (negated: none of them)
Keys, Valuescontinue with the keys or values as a collection

The samples on this page use the following dictionary:

Dictionary<string, int> playCounts = new() { { "Let It Be", 42 }, { "Yesterday", 7 } };

Equality​

You can verify that a dictionary is equal to another one. The entries are compared by key, so the order in which the two dictionaries enumerate them does not matter:

Dictionary<string, int> reordered = new() { { "Yesterday", 7 }, { "Let It Be", 42 } };

await That(playCounts).IsEqualTo(reordered);
await That(playCounts).IsNotEqualTo(new Dictionary<string, int> { { "Let It Be", 41 } });

The keys are looked up through the dictionary, so its key comparer decides which keys are the same:

Dictionary<string, int> ratings = new(StringComparer.OrdinalIgnoreCase) { { "Let It Be", 5 } };

await That(ratings).IsEqualTo(new Dictionary<string, int> { { "LET IT BE", 5 } });

To compare the entries in their enumeration order instead, compare them as a collection of KeyValuePair<TKey, TValue>:

SortedDictionary<string, int> sorted = new() { { "Let It Be", 42 }, { "Yesterday", 7 } };
KeyValuePair<string, int>[] expected = [new("Let It Be", 42), new("Yesterday", 7)];

await That(sorted.AsEnumerable()).IsEqualTo(expected);
Which key comparers are read

The comparer is read from the dictionary types of the framework and from the dictionary that a ReadOnlyDictionary<TKey, TValue> wraps, not from a custom dictionary or a ConcurrentDictionary<TKey, TValue> on .NET Framework. Before .NET 10, the wrapped dictionary is only reached by reflection, so it is not read in a project that enables trimming or Native AOT. For the others, a key of the dictionary that equals no expected key fails the expectation, even when the comparer considers it the same as one.

Entry​

You can verify that a dictionary contains the expected entry, which is looked up by its key, too:

await That(playCounts).Contains("Let It Be", 42);
await That(playCounts).Contains(new KeyValuePair<string, int>("Yesterday", 7));
await That(playCounts).DoesNotContain("Let It Be", 7);

Keys and values​

Contained​

You can verify that a dictionary contains the expected key(s) or value(s). Several keys or values can also be given as a collection:

await That(playCounts).ContainsKey("Let It Be");
await That(playCounts).ContainsKeys("Let It Be", "Yesterday");
await That(playCounts).DoesNotContainKey("Help!");
await That(playCounts).DoesNotContainKeys("Help!", "Something");
await That(playCounts).ContainsValue(42);
await That(playCounts).ContainsValues(42, 7);
await That(playCounts).DoesNotContainValue(0);
await That(playCounts).DoesNotContainValues(0, 1);
await That(playCounts).ContainsKeys(new List<string> { "Let It Be", "Yesterday" });

After ContainsKey or ContainsKeys, you can add expectations on the corresponding value(s). WhoseValues is a collection of the values for the expected keys, so all collection expectations are available:

await That(playCounts).ContainsKey("Let It Be").WhoseValue.IsEqualTo(42);
await That(playCounts).ContainsKeys("Let It Be", "Yesterday").WhoseValues.IsEqualTo([42, 7]);
await That(playCounts).ContainsKeys("Let It Be", "Yesterday")
.WhoseValues.All().ComplyWith(v => v.IsPositive());

The values, also the value of an entry, are compared with the same options as the items of a collection, e.g. IgnoringCase() for strings or a tolerance with Within:

Dictionary<int, string> titles = new() { { 1, "Let It Be" }, { 2, "Yesterday" } };
Dictionary<string, double> durations = new() { { "Let It Be", 3.85 }, { "Yesterday", 2.07 } };

await That(titles).ContainsValue("let it be").IgnoringCase();
await That(titles).ContainsValues("LET IT BE", "YESTERDAY").IgnoringCase();
await That(titles).Contains(1, "LET IT BE").IgnoringCase();
await That(durations).Contains("Yesterday", 2.0).Within(0.1);

DoesNotContainKeys and DoesNotContainValues mean "none of": they fail as soon as the dictionary contains one of them. This is stricter than negating ContainsKeys or ContainsValues with DoesNotComplyWith, which is the exact inverse ("not all") and only fails when the dictionary contains all of them:

await That(playCounts).DoesNotComplyWith(d => d.ContainsKeys("Let It Be", "Help!"))
.Because("the key \"Help!\" is missing, although \"Let It Be\" is contained");
await That(playCounts).DoesNotContainKeys("Help!", "Something")
.Because("none of the keys is contained, while adding \"Let It Be\" would fail");

As collections​

C# 14 or later

Keys and Values are extension properties, which need C# 14 or later, the default only for .NET 10 and later. With an older language version, pass the keys or values themselves, e.g. That(playCounts.Values).All().AreUnique(); the keys then no longer use the key comparer of the dictionary.

You can continue with expectations on the keys or on the values of a dictionary:

await That(playCounts).Keys.Contains("Let It Be");
await That(playCounts).Values.All().AreUnique();

The keys keep the key comparer of the dictionary, so e.g. Keys.Contains("LET IT BE") succeeds for a key "Let It Be" in a dictionary created with StringComparer.OrdinalIgnoreCase.

The keys of a dictionary are unique by design, so its entries are unique as well. To verify that its values are unique, use Values.All().AreUnique().

ConcurrentDictionary on .NET Framework

On .NET Framework, the comparer of a ConcurrentDictionary<TKey, TValue> is not read.