5

Is there a way to compare nested data structures in unittest but ignoring concrete types of objects, like assertSequenceEqual but recursive, for example:

#!/usr/bin/env python

import unittest

class DeepCompareTestCase(unittest.TestCase):

    def test_compare(self):
        # this test fails
        self.assertSequenceEqual(
            [['abc', 'def']],
            (('abc', 'def'),)
        )

unittest.main()

(something like Test::Deep in Perl)

0

3 Answers 3

1

Would something like this work?

import unittest
from itertools import zip_longest

class RecursiveTestCase(unittest.TestCase):
    def assertSequenceDeepEqual(self, x, y):
        MissingValue = MissingValueSentinel()
        for x, y, in zip_longest(x, y, fillvalue=MissingValue):
            try:
                self.assertSequenceDeepEqual(self, x, y)
            except TypeError:
                self.assertEqual(x, y)

                self.assertIsNot(x, MissingValue)
                self.assertIsNot(y, MissingValue)

class MissingValueSentinel(object):
    pass

zip_longest raises TypeError if one or more of the items is not an iterator, indicating you're at the bottom of the recursion.

If one of the iterators is shorter than the other, I have it returning a custom MissingValueSentinel object, which is detected by the assertIsNot calls at the end of the function. The only reason for this would be if x or y has a class that for whatever reason compares equal to even ad-hoc classes like MissingValueSentinel, but where assertEqual is still meaningful between two different objects of that class. That seems like a pretty weird edge case and you can probably safely ignore it.

Using zip_longest instead of zip prevents [1, 2, 3] incorrectly matching [1, 2].

Sign up to request clarification or add additional context in comments.

Comments

1

I had a similar problem to yours, but in my case I was testing the data structures delivered to an API endpoint. What I ended up doing was comparing a standardised JSON serialisation of each of the values. It's not a general solution since it will throw an exception on any value that can't be serialised to JSON, but in my case that's a feature rather than a bug and it does work in your example case, so I thought I'd share.

I created a tests/assertions.py file containing this code:

import json

class ApiAssertionsMixin(object):

    def assertJsonEqual(self, first, second, msg=None):
        j1 = json.dumps(first, sort_keys=True, indent=4)
        j2 = json.dumps(second, sort_keys=True, indent=4)
        self.maxDiff = None
        self.assertEqual(j1, j2, msg)

In your example, you'd use it like this:

import unittest
from tests.assertions import ApiAssertionsMixin

class DeepCompareTestCase(ApiAssertionsMixin, unittest.TestCase):

    def test_compare(self):
        self.assertJsonEqual(
            [['abc', 'def']],
            (('abc', 'def'),)
        )

unittest.main()

Which should pass. Here's an example of a failing test:

    def test_deep_compare(self):
        self.assertJsonEqual(
            { 'name': 'Bob', 'aliases': ['Kate', 'Robbie'], 'age': 19 },
            { 'name': 'Bob', 'age': 20, 'aliases': ['Robbie'] }
        )

With output like this:

.F
======================================================================
FAIL: test_deep_compare (__main__.DeepCompareTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "tests/test_nested.py", line 18, in test_deep_compare
    { 'name': 'Bob', 'age': 20, 'aliases': ['Robbie'] }
  File "./tests/assertions.py", line 10, in assertJsonEqual
    self.assertEqual(j1, j2, msg)
AssertionError: '{\n    "age": 19,\n    "aliases": [\n        "Kate",\n [41 chars]"\n}' != '{\n    "age": 20,\n    "aliases": [\n        "Robbie"\n[24 chars]"\n}'
  {
-     "age": 19,
?            ^^
+     "age": 20,
?            ^^
      "aliases": [
-         "Kate",
          "Robbie"
      ],
      "name": "Bob"
  }

----------------------------------------------------------------------
Ran 2 tests in 0.001s

FAILED (failures=1)

As you can see, in the event of a failure, you get plenty of context to work out what went wrong.

One downside of this approach is that the output from the test is JSON rather than Python, so if you copy/paste from the output to fix a test, you'll need to translate: true => True, false => False, null => None.

Comments

0

Maybe deepdiff module helps, but I did't test it.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.