tpierrain/NFluent

IsEquivalentTo for Dictionaries does not work correctly

bacar opened this issue · 3 comments

bacar commented

IsEquivalentTo has inconsistent behaviour for dictionaries, because it appears to do reference equality (not value equality) checks on keys.

The equivalence check should follow dictionary's key lookup, i.e. value equality.

Expected Behavior

All of these should pass

        var si1 = new Dictionary<string, int> { { "a", 0 }, { "b", 1 }, { "c", 2 } };
        var si2 = new Dictionary<string, int> { { "c", 2 }, { "a", 0 }, { "b", 1 } };
        Check.That(si1).IsEquivalentTo(si2);

        var letterA = "a";
        var letterAanotherWay = new string("a".ToCharArray()); // forces unique instance
        var sinotinterned1 = new Dictionary<string, int> { { letterA, 0 }, { "b", 1 }, { "c", 2 } };
        var sinotinterned2 = new Dictionary<string, int> { { "c", 2 }, { letterAanotherWay, 0 }, { "b", 1 } };
        Check.That(sinotinterned1).IsEquivalentTo(sinotinterned2);

        var is1 = new Dictionary<int, string> { { 1, "va" }, { 2, "vb" }, { 0, "vc" } };
        var is2 = new Dictionary<int, string> { { 0, "vc" }, { 1, "va" }, { 2, "vb" } };
        Check.That(is1).IsEquivalentTo(is2);

Current Behavior

Case 1

        var si1 = new Dictionary<string, int> { { "a", 0 }, { "b", 1 }, { "c", 2 } };
        var si2 = new Dictionary<string, int> { { "c", 2 }, { "a", 0 }, { "b", 1 } };
        Check.That(si1).IsEquivalentTo(si2);

This case passes because string literals used in the keys have reference equality.

Case 2

        var letterA = "a";
        var letterAanotherWay = new string("a".ToCharArray()); // forces unique instance
        var sinotinterned1 = new Dictionary<string, int> { { letterA, 0 }, { "b", 1 }, { "c", 2 } };
        var sinotinterned2 = new Dictionary<string, int> { { "c", 2 }, { letterAanotherWay, 0 }, { "b", 1 } };
        Check.That(sinotinterned1).IsEquivalentTo(sinotinterned2);

This case fails with:

        The checked dictionary is not equivalent to the expected one.
        actual's key "a" value should not exist (value 0)
        The checked dictionary:
            {[a, 0], [b, 1], [c, 2]} (3 items)
        The expected dictionary:
            {[c, 2], [a, 0], [b, 1]} (3 items)

...because even though the keys are equivalent, they are no longer reference equal (as you would get if you were e.g. reading the strings over the wire, from file, DB, etc...)

Case 3

        var is1 = new Dictionary<int, string> { { 1, "va" }, { 2, "vb" }, { 0, "vc" } };
        var is2 = new Dictionary<int, string> { { 0, "vc" }, { 1, "va" }, { 2, "vb" } };
        Check.That(is1).IsEquivalentTo(is2);

This case fails with:

        The checked dictionary is not equivalent to the expected one. 3 differences found!
        actual's key 1 value should not exist (value "va")
        actual's key 2 value should not exist (value "vb")
        actual's key 0 value should not exist (value "vc")
        The checked dictionary:
            {[1, va], [2, vb], [0, vc]} (3 items)
        The expected dictionary:
            {[0, vc], [1, va], [2, vb]} (3 items)

...because the dictionary is wrapped in a DictionaryWrapper<object, object>, which boxes the keys, and then it performs a reference equality check on the boxed key vs the boxed other key, which may returns false even if the keys are equivalent.

Possible Solution

I think the problem is in NFluent.Helpers.EqualityHelper.ValueDifferenceDictionary

var expectedIndex = expectedDico.ContainsKey(actualKey) ? expectedDico.Keys.ToList().FindIndex(x => x == actualKey) : -1;

The == performs reference equality on object.

Context

Was working in 2.4.0, this has now prevented upgrade from 2.4.0 -> 2.7.1
Which is a shame as I rather wanted to upgrade because in 2.4.0, Check.That(0.5d).IsZero() passes.

Your Environment

  • Version used: 2.7.1
  • Framework version: .net 4.8
bacar commented

2.6.0 does not appear to exhibit this problem

Hi, thanks again for this great report. I have devised a quick fix and I will publish a beta version shortly for you to confirm it fixes your issue.

Fixed in V2.7.2