20

I am trying to assert that two dictionaries are almost equal, but I can't seem to do that.

Here is an example:

>>> import nose.tools as nt
>>> nt.assert_dict_equal({'a' : 12.4}, {'a' : 5.6 + 6.8})
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib/python2.7/unittest/case.py", line 838, in assertDictEqual
    self.fail(self._formatMessage(msg, standardMsg))
  File "/usr/lib/python2.7/unittest/case.py", line 413, in fail
    raise self.failureException(msg)
AssertionError: {'a': 12.4} != {'a': 12.399999999999999}
- {'a': 12.4}
+ {'a': 12.399999999999999}

I would like this to pass, like that:

>>> nt.assert_almost_equal(12.4, 5.6 + 6.8)

I am hoping that I missing something simple like, nt.assert_almost_dict_equal, or maybe there is parameter that I could pass to nt.assert_dict_equal that specifies how close floating points should be, but I can't find anything.

Of course, I could just loop over the dictionaries and use nt.assert_almost_equal to compare the values individually; however, in my case the dictionary is more complicated, so I was hoping to avoid that.

What is the best way to assert that two dictionaries are almost equal?

Tim
  • 41,901
  • 18
  • 127
  • 145
Akavall
  • 82,592
  • 51
  • 207
  • 251
  • 2
    I think you will need to iterate and compare the values yourself. `assert_almost_equal` is only provided for numeric types whose difference can be directly computed. – BrenBarn May 08 '14 at 18:20
  • 7
    If you find you need to roll your own, check out "assertDeepAlmostEqual" here: https://github.com/larsbutler/oq-engine/blob/master/tests/utils/helpers.py – dano May 08 '14 at 18:20
  • @dano, that's interesting thanks. – Akavall May 08 '14 at 18:22

4 Answers4

20

The comment by @dano answered my question:

I copied a function from a link provided by dano

import unittest
import numpy

def assertDeepAlmostEqual(test_case, expected, actual, *args, **kwargs):
    """
    Assert that two complex structures have almost equal contents.

    Compares lists, dicts and tuples recursively. Checks numeric values
    using test_case's :py:meth:`unittest.TestCase.assertAlmostEqual` and
    checks all other values with :py:meth:`unittest.TestCase.assertEqual`.
    Accepts additional positional and keyword arguments and pass those
    intact to assertAlmostEqual() (that's how you specify comparison
    precision).

    :param test_case: TestCase object on which we can call all of the basic
    'assert' methods.
    :type test_case: :py:class:`unittest.TestCase` object
    """
    is_root = not '__trace' in kwargs
    trace = kwargs.pop('__trace', 'ROOT')
    try:
        if isinstance(expected, (int, float, long, complex)):
            test_case.assertAlmostEqual(expected, actual, *args, **kwargs)
        elif isinstance(expected, (list, tuple, numpy.ndarray)):
            test_case.assertEqual(len(expected), len(actual))
            for index in xrange(len(expected)):
                v1, v2 = expected[index], actual[index]
                assertDeepAlmostEqual(test_case, v1, v2,
                                      __trace=repr(index), *args, **kwargs)
        elif isinstance(expected, dict):
            test_case.assertEqual(set(expected), set(actual))
            for key in expected:
                assertDeepAlmostEqual(test_case, expected[key], actual[key],
                                      __trace=repr(key), *args, **kwargs)
        else:
            test_case.assertEqual(expected, actual)
    except AssertionError as exc:
        exc.__dict__.setdefault('traces', []).append(trace)
        if is_root:
            trace = ' -> '.join(reversed(exc.traces))
            exc = AssertionError("%s\nTRACE: %s" % (exc.message, trace))
        raise exc

# My part, using the function

class TestMyClass(unittest.TestCase):
    def test_dicts(self):
        assertDeepAlmostEqual(self, {'a' : 12.4}, {'a' : 5.6 + 6.8})
    def test_dicts_2(self):
        dict_1 = {'a' : {'b' : [12.4, 0.3]}}
        dict_2 = {'a' : {'b' : [5.6 + 6.8, 0.1 + 0.2]}}

        assertDeepAlmostEqual(self, dict_1, dict_2)

def main():
    unittest.main()

if __name__ == "__main__":
    main()

Result:

Ran 2 tests in 0.000s

OK
Akavall
  • 82,592
  • 51
  • 207
  • 251
  • 3
    Downvoter, can you explain the reason for the downvote. I believe self-answers are fine. Do you think I am not giving enough credit to @dano? – Akavall May 08 '14 at 19:01
  • 2
    It's probably bad style, but if you monkey-patch TestCase via 'unittest.TestCase.assertDeepAlmostEqual = assertDeepAlmostEqual` then you can use the test like any other, e.g. `self.assertDeepAlmostEqual(dict_1, dict_2)` – patricksurry Oct 21 '14 at 14:58
  • Should also note that in my case I had to sort all nested collection, so you could use the code here together with this code snippet for [ordering nested collection](http://stackoverflow.com/a/25851972/929164) – Daniel Dror Sep 05 '16 at 08:17
  • @patricksurry it's probably the case for turning this into a mixin – Elias Dorneles Sep 20 '17 at 18:41
  • 2
    No need to monkey patch `TestCase`. You can simply subclass it. Indeed that's what `testtools.TestCase` does. – Helmut Grohne Mar 27 '18 at 09:55
  • 1
    Python 2 is no longer supported so I would replace `xrange` with `range`. Also `long` should be `numpy.long` or you can explicitly `from numpy import long, ndarray` and save importing all of numpy. – Zemogle Feb 25 '20 at 11:45
4

Pytest "approx" does the job

In [10]: {'a': 2.000001} == pytest.approx({'a': 2}) Out[10]: True

Arnaud Fouchet
  • 933
  • 1
  • 7
  • 14
3

I realize you wouldn't import pandas just to do this but if you happen to be using pandas you can convert the dicts to series and use the assert_series_equal from pandas.testing which, by default, has check_exact=False.

>>> import pandas as pd
>>> from pandas.testing import assert_series_equal
>>> a = pd.Series({'a' : 12.4})
>>> b = pd.Series({'a': 12.399999999999999})
>>> assert_series_equal(a, b)
Bill
  • 10,323
  • 10
  • 62
  • 85
1

I couldn't get Akavall's function to run so made my own. Is a little too simple but works for my purposes. Code to test that function is working written using pytest

from numbers import Number
from math import isclose

def dictsAlmostEqual(dict1, dict2, rel_tol=1e-8):
    """
    If dictionary value is a number, then check that the numbers are almost equal, otherwise check if values are exactly equal
    Note: does not currently try converting strings to digits and comparing them. Does not care about ordering of keys in dictionaries
    Just returns true or false
    """
    if len(dict1) != len(dict2):
        return False
    # Loop through each item in the first dict and compare it to the second dict
    for key, item in dict1.items():
        # If it is a nested dictionary, need to call the function again
        if isinstance(item, dict):
            # If the nested dictionaries are not almost equal, return False
            if not dictsAlmostEqual(dict1[key], dict2[key], rel_tol=rel_tol):
                return False
        # If it's not a dictionary, then continue comparing
        # Put in else statement or else the nested dictionary will get compared twice and
        # On the second time will check for exactly equal and will fail
        else:
            # If the value is a number, check if they are approximately equal
            if isinstance(item, Number):
                # if not abs(dict1[key] - dict2[key]) <= rel_tol:
                # https://stackoverflow.com/questions/5595425/what-is-the-best-way-to-compare-floats-for-almost-equality-in-python
                if not isclose(dict1[key], dict2[key], rel_tol=rel_tol):
                    return False
            else:
                if not (dict1[key] == dict2[key]):
                    return False
    return True

Validate function output using pytest

import pytest
import dictsAlmostEqual
def test_dictsAlmostEqual():
    a = {}
    b = {}
    assert dictsAlmostEqual(a, b)
    a = {"1": "a"}
    b = {}
    assert not dictsAlmostEqual(a, b)
    a = {"1": "a"}
    b = {"1": "a"}
    assert dictsAlmostEqual(a, b)
    a = {"1": "a"}
    b = {"1": "b"}
    assert not dictsAlmostEqual(a, b)
    a = {"1": "1.23"}
    b = {"1": "1.23"}
    assert dictsAlmostEqual(a, b)
    a = {"1": "1.234"}
    b = {"1": "1.23"}
    assert not dictsAlmostEqual(a, b)
    a = {"1": 1.000000000000001, "2": "a"}
    b = {"1": 1.000000000000002, "2": "a"}
    assert not dictsAlmostEqual(a, b, rel_tol=1e-20)
    assert dictsAlmostEqual(a, b, rel_tol=1e-8)
    assert dictsAlmostEqual(a, b)
    # Nested dicts
    a = {"1": {"2": 1.000000000000001}}
    b = {"1": {"2": 1.000000000000002}}
    assert not dictsAlmostEqual(a, b, rel_tol=1e-20)
    assert dictsAlmostEqual(a, b, rel_tol=1e-8)
    assert dictsAlmostEqual(a, b)
    a = {"1": {"2": 1.000000000000001, "3": "a"}, "2": "1.23"}
    b = {"1": {"2": 1.000000000000002, "3": "a"}, "2": "1.23"}
    assert not dictsAlmostEqual(a, b, rel_tol=1e-20)
    assert dictsAlmostEqual(a, b, rel_tol=1e-8)
    assert dictsAlmostEqual(a, b)
Viragos
  • 561
  • 6
  • 15