Search code examples
pythonjsonmocking

How to make a Python MagicMock object JSON serializable


I am using a Python Mock object for a third-party package that needs to JSON serialize my mock. This means that I cannot change the invocation of json.dumps, so must use the solution here: https://stackoverflow.com/a/31207881/19643198

class FileItem(dict):
    def __init__(self, fname):
        dict.__init__(self, fname=fname)

f = FileItem('tasks.txt')
json.dumps(f)  #No need to change anything here

The only problem is that my object is not of class FileItem, but needs to be a MagicMock. This suggests multiple inheritance, so something like:

class FileItem(MagicMock, dict):
    def __init__(self):
        MagicMock.__init__(self) 
        dict.__init__(self)

Unfortunately, multiple inheritance from both dict and MagicMock seems not to work. In case this helps make this problem easier to solve, the third-party library does not need to deserialize or even use the JSON serialized representation of the MagicMock.


Solution

  • There is no need to consider multiple inheritance. The problem is that Python's JSON module doesn't know how to serialize certain types, like MagicMock (or other common types like datetime, for that matter).

    You can tell json how to deal with unknown types by either using the cls= or default= parameters, but since you are asking about unit tests and don't want to modify the code you're testing, you can solve this by also mocking json.dumps (or json.dump).

    For instance,

    import json
    from json import dumps as _dumps
    from unittest.mock import MagicMock, Mock
    
    f1 = {'fname': 'tasks.txt'}  # a normal dict
    f2 = {'fname': MagicMock()}  # some value containing a MagicMock instance
    
    def dumps_wrapper(*args, **kwargs):
        return _dumps(*args, **(kwargs | {"default": lambda obj: "mock"}))
    
    # mock the `dumps` function
    json.dumps = MagicMock(wraps=dumps_wrapper)
    
    # now you can serialize objects containing MagicMock (or Mock) objects
    json.dumps(f1)  # {"fname": "tasks.txt"}
    json.dumps(f2)  # {"fname": "mock"}
    

    In your unit tests, you can use the patch function to temporarily patch the json.dumps function:

    # my_module.py
    import json
    def make_json(obj):
        return json.dumps(obj)
    
    # test_mocked_json.py
    import unittest
    from unittest.mock import MagicMock, patch
    from json import dumps as _dumps
    
    import my_module
    
    def dumps_wrapper(*args, **kwargs):
        return _dumps(*args, **(kwargs | {"default": lambda obj: "mock"}))
    
    class TestMockedJSON(unittest.TestCase):
        def test_mocked_json(self):
    
            patch_json = patch(
                "my_module.json.dumps",
                MagicMock(wraps=dumps_wrapper))
    
            with patch_json:
                f1 = {'fname': 'tasks.txt'}  # a normal dict
                f2 = {'fname': MagicMock()}  # some value containing a MagicMock instance
                s1 = my_module.make_json(f1)
                s2 = my_module.make_json(f2)
                assert s1
                assert s2
                print(s1, s2, "ok!")
    
    

    Which you can run with:

    python -m unittest test_mocked_json.py
    
    # {"fname": "tasks.txt"} {"fname": "mock"} ok!
    # .
    # ----------------------------------------------------------------------
    # Ran 1 test in 0.001s
    # 
    # OK