20

I know that, when performing assertEqual on a dictionary, assertDictEqual is called. Similarly, assertEqual on a sequence will perform assertSequenceEqual.

However, when assertDictEqual is comparing values, it appears not to make use of assertEqual, and thus assertSequenceEqual is not called.

Consider the following simple dictionaries:

lst1 = [1, 2]
lst2 = [2, 1]

d1 = {'key': lst1}
d2 = {'key': lst2}

self.assertEqual(lst1, lst2) # True
self.assertEqual(d1, d2) # False ><

How can I test dictionaries such as d1 and d2 such that their equality is properly compared, by recursively applying assertEqual-like semantics to values?

I want to avoid using external modules (as suggested in this question) if at all possible, unless they are native django extensions.


EDIT

Essentially, what I am after is a built in version of this:

def assertDictEqualUnorderedValues(self, d1, d2):
    for k,v1 in d1.iteritems():
        if k not in d2:
            self.fail('Key %s missing in %s'%(k, d2))

        v2 = d2[k]

        if isinstance(v1, Collections.iterable) and not isinstance(v1, basestring):
            self.assertValuesEqual(v1, v2)
        else:
            self.assertEqual(v1, v2)

The problem with the above code is that the error messages are not as nice as the builtin asserts, and there's probably edge cases I've ignored (as I just wrote that off the top of my head).

Community
  • 1
  • 1
sapi
  • 9,944
  • 8
  • 41
  • 71
  • 3
    With the `unittest` module, `self.assertEqual(lst1, lst2)` isn't True --> `AssertionError: Lists differ: [1, 2] != [2, 1]`. – martineau Aug 27 '13 at 12:15
  • @martineau - my mistake; I misread that part of the documentation. I am looking for an equivalent of `assertItemsEqual` rather than `assertSequenceEqual` – sapi Aug 27 '13 at 13:55
  • 1
    Well, if you make `lst1` and `lst2` the same so the first `assertEqual` succeeds, then the second one will, too. – martineau Aug 27 '13 at 14:57
  • The part about "looking for an equivalent of `assertItemsEqual` rather than `assertSequenceEqual`" makes no sense to me. My point was that the code in your question is using `assertEqual` on two lists with items in a different order and that assertion will fail. – martineau Aug 28 '13 at 02:14
  • If you control the code creating the datastructures, maybe you should use sets instead of lists. – Quantum7 Aug 07 '19 at 10:08

3 Answers3

8

Rather than overriding assertDictEqual, why don't you recursively sort your dicts first?

def deep_sort(obj):
    """
    Recursively sort list or dict nested lists
    """

    if isinstance(obj, dict):
        _sorted = {}
        for key in sorted(obj):
            _sorted[key] = deep_sort(obj[key])

    elif isinstance(obj, list):
        new_list = []
        for val in obj:
            new_list.append(deep_sort(val))
        _sorted = sorted(new_list)

    else:
        _sorted = obj

    return _sorted

Then sort, and use normal assertDictEqual:

    dict1 = deep_sort(dict1)
    dict2 = deep_sort(dict2)

    self.assertDictEqual(dict1, dict2)

This approach has the benefit of not caring about how many levels deep your lists are.

Chris Villa
  • 3,901
  • 1
  • 18
  • 10
5

The TestCase.assertEqual() method calls the class' assertDictEqual() for dicts, so just override that in your subclass derivation. If you only use other assertXXX methods in the method, the error messages should be almost as nice as the built-in asserts -- but if not you can provide a msg keyword argument when you call them to control what is displayed.

import collections
import unittest

class TestSOquestion(unittest.TestCase):

    def setUp(self):
        pass # whatever...

    def assertDictEqual(self, d1, d2, msg=None): # assertEqual uses for dicts
        for k,v1 in d1.iteritems():
            self.assertIn(k, d2, msg)
            v2 = d2[k]
            if(isinstance(v1, collections.Iterable) and
               not isinstance(v1, basestring)):
                self.assertItemsEqual(v1, v2, msg)
            else:
                self.assertEqual(v1, v2, msg)
        return True

    def test_stuff(self):
        lst1 = [1, 2]
        lst2 = [2, 1]

        d1 = {'key': lst1}
        d2 = {'key': lst2}

        self.assertItemsEqual(lst1, lst2) # True
        self.assertEqual(d1, d2) # True

if __name__ == '__main__':
    unittest.main()

Output:

> python unittest_test.py
.
---------------------------------------------------------------------->
Ran 1 test in 0.000s

OK

>
martineau
  • 119,623
  • 25
  • 170
  • 301
  • 1
    I cannot guarantee the order of the lists. The tests are for a django framework, and I can't rely on the order of database queries being the same between the test set and the expected result. All I care about is that the API is giving me the correct values in *some* order. – sapi Aug 27 '13 at 22:35
  • OK, but I still don't understand why/how you expect the first `assertEqual(lst1, lst2)` to be True in your code. – martineau Aug 28 '13 at 01:51
  • This solution does not work on deeply nested dicts + lists. The solution based on sorting does. – Federico Jul 21 '15 at 22:01
1

I had the same problem, i had to test if the fields of a model where correct. And MyModel._meta.get_all_field_names() sometimes returns ['a','b'] and sometimes ['b','a'].

When i run:

self.assertEqual(MyModel._meta.get_all_field_names(), ['a', 'b'])

it sometimes fails.

I solved it by putting both values in a set():

self.assertEqual(set(MyModel._meta.get_all_field_names()), set(['a', 'b'])) #true

self.assertEqual(set(MyModel._meta.get_all_field_names()), set(['b', 'a'])) #true

This will not work (returns True) with:

self.assertEqual(set(['a','a','b','a']), set(['a','b']))  # Also true 

But since i'm checking for field names of a model , and those are unique, this is good by me.

Robin van Leeuwen
  • 2,625
  • 3
  • 24
  • 35
  • You could use set notation to improve this, instead of `set(['a','b'])` try `{'a','b'}`. Also, there is a function `self.assertSetEqual({'a','a','b','a'}, {'a','b'})` which will check the difference between the two sets and fail if they do not contain exactly the same elements. – Mouscellaneous Aug 04 '16 at 11:13