简体   繁体   中英

How to mock python dict __str__ method?

I have a method which takes as a parameter a dict which needs to contain certain entries. For example, I may expect a type key in the dict whose value needs to be one of ['typeA', 'typeB', 'typeC'] . If a key is missing, I have the method throw an exception, SpecFormatException . The exception message should read something like 'type' entry is missing in spec dict: {'foo': 'bar', 'baz': [1, 2, 3]} .

I am writing a test to verify that the exception is being thrown with the correct error message, but since the output of the dict.__str__() method is nondeterministic, my test is flaky. I thought that if I could patch the dict.__str__() method to output something like "YES I AM YOUR DICTIONARY" then I could fix the flakiness, but when I try to patch my test with:

import mock
import my_verifier

@mock.patch('my_verifier.dict')
def testErrorMessage(this, dict_str):
  dict_str.return_value = 'YES I AM YOUR DICTIONARY'
  ...

I get the error message "my_verifier.py does not have the attribute 'dict'" when trying to run my test.

I assume mocking the __str__ method is the right approach here, but how exactly do I do it?

How specific does your test for the error message need to be? Could you maybe just assert that some part of what you expect is in there, such as:

import pytest

def test_my_method_error_message():
    offending_key = 'test'
    data = {offending_key: 'whatever'}
    with pytest.raises(SpecFormatException) as excinfo:
        my_method(data)
        assert data in str(excinfo.value)

Obviously, you might need to test other parts of the message as well. Testing for the EXACT string may or not be what you want to do, as that message might change. If you need to go down that route, I would suggest maybe storing the strings (or string templates) somewhere that can be accessed by everything, so that your test can make it's assertion based on a class variable or configuration parameter.

If replacing the built-in dict.__str__ method with a custom dict2str function is an option, here's one that sorts keys alphabetically:

def det_str(x):
    if isinstance(x, dict):
        return '{{{}}}'.format(', '.join('{!r}: {}'.format(k, det_str(v)) for k, v in sorted(x.items())))
    else:
        return str(x)

>>> det_str({'a': 1, 'b': 0, 'c': -1})
"{'a': 1, 'b': 0, 'c': -1}"
>>> det_str({'c': -1, 'a': 1, 'b': 0})
"{'a': 1, 'b': 0, 'c': -1}"

works also recursively:

>>> det_str({'b': 0, 'a': 1, 'c': -1, 'd': {'z': 10, 'x': 20, 'y': -30}})
"{'a': 1, 'b': 0, 'c': -1, 'd': {'x': 20, 'y': -30, 'z': 10}}"

I'm going to assume you pass a dictionary as an argument when you raise the exception, something like

d = {'foo': 'bar', 'baz': [1, 2, 3]}
if not d.get('type') in ['typeA', 'typeB', 'typeC']:
    raise SpecFormatException(d)

The first test will focus solely on the exception itself, which means the argument doesn't really matter, as long as it is something that has a __str__ method:

def testExceptionMessage(self):
    d = mock.MagicMock()
    exc = SpecFormatException(d)
    observed = str(exc)
    self.assertEqual(observed,
                     "'type' entry is missing in spec dict: {}".format(d)

If you check the value of d.__str__.called before and after calling str(exc) , you should see the value change from False to True , verifying that SpecFormatException does indeed use the argument's __str__ method to create the value of observed .

The second test focuses on the exception being raised as expected. You don't really need to check what its message is, because 1) you've already tested that elsewhere and 2) the message doesn't really contain any information that you can't glean from the dict instance itself and the fact that SpecFormatException is, indeed, raised.

def testRaiseException(self):
    with self.assertRaises(SpecFormatException):
        # code that should cause SpecFormatException be raised

This is an older question, but I had the same difficulty and couldn't find a working answer. I ended up using unittest.mock.MagicMock to mock the __str__ output of an object's function. MagicMock is designed to mock Python's magic functions, such as __str__ .

You shouldn't need to use anything like SpecFormatException for the test itself. You can use mock.patch as a context manager -- for your code, this would look like:

import my_verifier
import unittest.mock


def test_my_method_error_message():
    with mock.patch("my_verifier.dict", mock.MagicMock()) as mock_dict:
        mock_dict.__str__.return_value = "YES I AM YOUR DICTIONARY"
        assert str(mock_dict) == "YES I AM YOUR DICTIONARY"

        # ...other tests that rely on a mocked instance of my_verifier.dict...

    # ...other tests that don't rely on a mocked instance of my_verifier.dict...

Assuming that my_verifier has a dict object/method, this allows you to control the output of printing my_verifier.dict for one or more tests, but without fully replacing my_verifier.dict with a mock object.

You can also mock other (magic) functions of my_verifier.dict inside the same context, and have any other tests reside outside the context.

The patch.object method may also be of use.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM