Skip to content

Dictionary equivalence with mismatched keys #2350

@wuzzeb

Description

@wuzzeb

An assertion of equivalence using ImmutableDictionaries gives a KeyNotFoundException instead of a ComparisonFailure

await Assert.That(ImmutableDictionary<string, int>.Empty.Add("Hello", 1))
  .IsEquivalentTo(ImmutableDictionary<string, int>.Empty.Add("Hello2", 1));

Produces

      System.Collections.Generic.KeyNotFoundException : The given key 'Hello' was not present in the dictionary.
      Stack Trace:
           at System.Collections.ThrowHelper.ThrowKeyNotFoundException[TKey](TKey key)
           at System.Collections.Immutable.ImmutableDictionary`2.get_Item(TKey key)
           at System.Collections.Immutable.ImmutableDictionary`2.System.Collections.IDictionary.get_Item(Object key)
        /home/wuzzeb/projects/TUnit/TUnit.Assertions/Compare.cs(95,0): at TUnit.Assertions.Compare.CheckEquivalent[TActual,TExpected](TActual actual, TExpected expected, CompareOptions options, String[] memberNames, MemberType memberType, HashSet`1 visited)+MoveNext()
           at System.Collections.Generic.List`1..ctor(IEnumerable`1 collection)
           at System.Linq.Enumerable.ToList[TSource](IEnumerable`1 source)
        /home/wuzzeb/projects/TUnit/TUnit.Assertions/Assertions/Generics/Conditions/EquivalentToExpectedValueAssertCondition.cs(81,0): at TUnit.Assertions.Assertions.Generics.Conditions.EquivalentToExpectedValueAssertCondition`2.GetResult(TActual actualValue, TExpected expectedValue)
        /home/wuzzeb/projects/TUnit/TUnit.Assertions/AssertConditions/ExpectedValueAssertCondition.cs(47,0): at TUnit.Assertions.AssertConditions.ExpectedValueAssertCondition`2.GetResult(TActual actualValue, Exception exception, AssertionMetadata assertionMetadata)
        /home/wuzzeb/projects/TUnit/TUnit.Assertions/AssertConditions/BaseAssertCondition.cs(91,0): at TUnit.Assertions.AssertConditions.BaseAssertCondition`1.GetAssertionResult(TActual actualValue, Exception exception, AssertionMetadata assertionMetadata, String actualExpression)
        /home/wuzzeb/projects/TUnit/TUnit.Assertions/AssertConditions/BaseAssertCondition.cs(72,0): at TUnit.Assertions.AssertConditions.BaseAssertCondition`1.GetAssertionResult(Object actualValue, Exception exception, AssertionMetadata assertionMetadata, String actualExpression)
        /home/wuzzeb/projects/TUnit/TUnit.Assertions/AssertionBuilders/AssertionBuilder.cs(142,0): at TUnit.Assertions.AssertionBuilders.AssertionBuilder.ProcessAssertionsAsync()
        /home/wuzzeb/projects/TUnit/TUnit.Assertions/AssertionBuilders/InvokableValueAssertionBuilder.cs(34,0): at TUnit.Assertions.AssertionBuilders.InvokableValueAssertionBuilder`1.AssertAndGet()

Which points to

var actualObject = actualDictionary[key];

It makes sense, you make a list of all keys in both dictionaries, in this case ["Hello", "Hello2"] and then look them up in both dictionaries and get a key not found. But I was confused for a long time because if you switch to just Dictionaries instead of ImmutableDictionaries, this code doesn't get that exception

await Assert.That(new Dictionary<string, int> {{"Hello", 1}})
  .IsEquivalentTo(new Dictionary<string, int> { { "Hello2", 1 } });

This correctly produces

      TUnit.Assertions.Exceptions.AssertionException : Expected new Dictionary<string, int> {{"Hello", 1}} to be equivalent to [[Hello2, 1]]
      
      but [Hello] did not match
      Expected: null
      Received: 1
      
      at Assert.That(new Dictionary<string, int> {{"Hello", 1}}).IsEquivalentTo(new Dictionary<string, int> {...

I finally realized that the non-generic System.Collections.IDictionary interface on Dictionary returns null instead of key not found, i.e. the following is true

var d = new Dictionary<string, int>();
d.Add("Hello", 1);

System.Collections.IDictionary d2 = d;

await Assert.That(() => d["Hello2"]).Throws<KeyNotFoundException>();
await Assert.That(d2["Hello2"]).IsNull();

which is why the code in Compare.cs works for Dictionaries because it is converting to the non-generic IDictionary interface before referencing the keys.

But, this means we can make two dictionaries seem to be equivalent if we put in nulls. The following code passes...

await Assert.That(new Dictionary<string, string?> {{"Hello", "a"}, {"Hello2", null}})
  .IsEquivalentTo(new Dictionary<string, string?> { { "Hello", "a" } });

because when the "Hello2" key is looked up in the expected dictionary, it gets null which matches.

At first I thought using TryGetValue instead, but that doesn't exist on the non-generic IDictionary. You could use Contains to first check the key, but I think something like the following can give better error messages

var actualKeys = new HashSet<object>(actualDictionary.Keys)

foreach (var expectedKV in expectedDictionary) {
    if (actualKeys.Contains(expectedKV.Key)) {
       check actualDictionary[expectedKV.Key] with expectedKV.Value
       actualKeys.Remove(expectedKV.Key)
    } else {
       error that expectedKV.Key was expected to exist but does not
   }
}
if (actualKeys.Count > 0) {
    error that extra keys beyond expected existed
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions