Search code examples
pythonunit-testingmockingpython-unittestkeyword-argument

mocking a function that accepts **kwargs


I am having trouble mocking a function that accepts **kwargs. The scenario is I have ClassA (which in my specific case is something I didnt write) and it has a function with **kwargs. ClassB which has an instance of ClassA, and calls the **kwargs function. I want to test ClassB by mocking the call to Class A's function.

Here is what I have tried so far, and in both of my attempts I ended up with a TypeError. Is there any way to do this? Should I rethink another aspect of this?

import unittest


#a class i have no control over. Has a function accepting **kwargs
class ClassA(object):

    def classFunctionAcceptingKwargs(self, **kwargs):
        return kwargs["a"] + kwargs["b"]

# a mock of the above class
class Mock_ClassA(object):

    def __init__(self):
        self.mockclassFunctionAcceptingKwargs = lambda **kwargs: None

    def classFunctionAcceptingKwargs(self, **kwargs):
        #FAILS: TypeError: mockFunctionAcceptingKwargs() takes exactly 0 arguments (1 given)
        return self.mockclassFunctionAcceptingKwargs(kwargs)
        #ALSO FAILS: TypeError: mockFunctionAcceptingKwargs() argument after ** must be a mapping, not set
        #return self.mockclassFunctionAcceptingKwargs(**{kwargs["a"] + kwargs["b"]})

#class B calls the class A kwargs but exposes a function with a dict
class ClassB(object):
    def __init__(self, classA):
        self.classA = classA

    def doSomething(self, dict):
        return self.classA.classFunctionAcceptingKwargs(**dict)

class TestClassA(unittest.TestCase):

    def runTest(self):
        a = ClassA()
        result = a.classFunctionAcceptingKwargs(**{"a":1, "b": 2})
        self.assertEqual(result, 3)

class TestClassB(unittest.TestCase):

    def runTest(self):

        mock = Mock_ClassA()
        def mockFunctionAcceptingKwargs(**kwargs):
            self.assertEqual(kwargs["a"], 1)
            self.assertEqual(kwargs["b"], 2)

        mock.mockclassFunctionAcceptingKwargs = mockFunctionAcceptingKwargs
        b = ClassB(mock)
        b.doSomething({"a": 1, "b": 2})

Stack trace:

Test Name:  TestClassB
Test Outcome:   Failed
Result StandardError:   
======================================================================
ERROR: runTest (module1.TestClassB)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\test\module1.py", line 49, in runTest
    b.doSomething({"a": 1, "b": 2})
  File "C:\test\module1.py", line 28, in doSomething
    return self.classA.classFunctionAcceptingKwargs(**dict)
  File "C:\test\module1.py", line 18, in classFunctionAcceptingKwargs
    return self.mockclassFunctionAcceptingKwargs(kwargs)
TypeError: mockFunctionAcceptingKwargs() takes exactly 0 arguments (1 given)
----------------------------------------------------------------------
Ran 1 test in 12.873s
FAILED (errors=1)

Solution

  • I'm not sure why you are calling another function. But if you must have an instance attribute mockclassFunctionAcceptingKwargs function, then just pass on the kwargs dictionary with **kwargs:

    class Mock_ClassA(object):
        # ...
        def classFunctionAcceptingKwargs(self, **kwargs):
            return self.mockclassFunctionAcceptingKwargs(**kwargs)
    

    You don't need to call that lambda at all if all you need is for classFunctionAcceptingKwargs to exist:

    class Mock_ClassA(object):
        def __init__(self, mock_result):
            self.mock_result = mock_result
        def classFunctionAcceptingKwargs(self, **kwargs):
            self.called_with = kwargs
            return self.mock_result
    

    then just pass in the mocked return value, whatever you need to be passed back to ClassB for the test, and afterwards you can check that the right value was passed in too:

    mock = Mock_ClassA(3)  # to return 3 back to the caller
    b = ClassB(mock)
    b.doSomething({"a": 1, "b": 2})
    self.assertEqual(mock.called_with, {'a': 1, 'b': 2})
    

    You may want to use the unittest.mock library to build a mock object to pass in (available in Python 3, and a backport for Python 2 can be installed). It'll let you create a mocked ClassA and then use the API to test if the mock was used in the expected manner:

    try:
        # Python 3
        from unittest import mock
    except ImportError:
        # Python 2, backport
        import mock
    
    class TestClassB(unittest.TestCase):
        def runTest(self):
            mockA = mock.Mock(spec=ClassA)  # only accept attributes ClassA also has
            mockA.classFunctionAcceptingKwargs.return_value = 3  # or whatever else you want it to return
            b = ClassB(mockA)
            b.doSomething({"a": 1, "b": 2})
            mockA.classFunctionAcceptingKwargs.assert_called_once_with(a=1, b=2)
    

    Demo with unittest.mock as the mocking layer:

    >>> from unittest import mock
    >>> class ClassA(object):
    ...     def classFunctionAcceptingKwargs(self, **kwargs):
    ...         return kwargs["a"] + kwargs["b"]
    ...
    >>> class ClassB(object):
    ...     def __init__(self, classA):
    ...         self.classA = classA
    ...     def doSomething(self, dict):
    ...         return self.classA.classFunctionAcceptingKwargs(**dict)
    ...
    >>> mockA = mock.Mock(spec=ClassA)
    >>> mockA.classFunctionAcceptingKwargs.return_value = 3
    >>> b = ClassB(mockA)
    >>> b.doSomething({"a": 1, "b": 2})
    3
    >>> mockA.classFunctionAcceptingKwargs.assert_called_once_with(a=1, b=2)  # passes, no exception raised
    >>> mockA.mock_calls
    [call.classFunctionAcceptingKwargs(a=1, b=2)]