Python Mock object with method called multiple times

47,518

Solution 1

Try side_effect

def my_side_effect(*args, **kwargs):
    if args[0] == 42:
        return "Called with 42"
    elif args[0] == 43:
        return "Called with 43"
    elif kwargs['foo'] == 7:
        return "Foo is seven"

mockobj.mockmethod.side_effect = my_side_effect

Solution 2

A little sweeter:

mockobj.method.side_effect = lambda x: {123: 100, 234: 10000}[x]

or for multiple arguments:

mockobj.method.side_effect = lambda *x: {(123, 234): 100, (234, 345): 10000}[x]

or with a default value:

mockobj.method.side_effect = lambda x: {123: 100, 234: 10000}.get(x, 20000)

or a combination of both:

mockobj.method.side_effect = lambda *x: {(123, 234): 100, (234, 345): 10000}.get(x, 20000)

and merrily on high we go.

Solution 3

I've ran into this when I was doing my own testing. If you don't care about capturing calls to your methodfromdepclass() but just need it to return something, then the following may suffice:

def makeFakeMethod(mapping={}):
    def fakeMethod(inputParam):
        return mapping[inputParam] if inputParam in mapping else MagicMock()
    return fakeMethod

mapping = {42:"Called with 42", 59:"Called with 59"}
mockobj.methodfromdepclass = makeFakeMethod(mapping)

Here's a parameterized version:

def makeFakeMethod():
    def fakeMethod(param):
        return "Called with " + str(param)
    return fakeMethod

Solution 4

As in here, apart from using side_effect in unittest.mock.Mock you can also use @mock.patch.object with new_callable, which allows you to patch an attribute of an object with a mock object.

Let's say a module my_module.py uses pandas to read from a database and we would like to test this module by mocking pd.read_sql_table method (which takes table_name as argument).

What you can do is to create (inside your test) a db_mock method that returns different objects depending on the argument provided:

def db_mock(**kwargs):
    if kwargs['table_name'] == 'table_1':
        # return some DataFrame
    elif kwargs['table_name'] == 'table_2':
        # return some other DataFrame

In your test function you then do:

import my_module as my_module_imported

@mock.patch.object(my_module_imported.pd, "read_sql_table", new_callable=lambda: db_mock)
def test_my_module(mock_read_sql_table):
    # You can now test any methods from `my_module`, e.g. `foo` and any call this 
    # method does to `read_sql_table` will be mocked by `db_mock`, e.g.
    ret = my_module_imported.foo(table_name='table_1')
    # `ret` is some DataFrame returned by `db_mock`
Share:
47,518
Adam Parkin
Author by

Adam Parkin

Blog Github Twitter (@codependentcodr) LinkedIn

Updated on July 05, 2022

Comments

  • Adam Parkin
    Adam Parkin almost 2 years

    I have a class that I'm testing which has as a dependency another class (an instance of which gets passed to the CUT's init method). I want to mock out this class using the Python Mock library.

    What I have is something like:

    mockobj = Mock(spec=MyDependencyClass)
    mockobj.methodfromdepclass.return_value = "the value I want the mock to return"
    assertTrue(mockobj.methodfromdepclass(42), "the value I want the mock to return")
    
    cutobj = ClassUnderTest(mockobj)
    

    Which is fine, but "methodfromdepclass" is a parameterized method, and as such I want to create a single mock object where depending on what arguments are passed to methodfromdepclass it returns different values.

    The reason I want this parameterized behaviour is I want to create multiple instances of ClassUnderTest that contain different values (the values of which are produced by what gets returned from the mockobj).

    Kinda what I'm thinking (this of course does not work):

    mockobj = Mock(spec=MyDependencyClass)
    mockobj.methodfromdepclass.ifcalledwith(42).return_value = "you called me with arg 42"
    mockobj.methodfromdepclass.ifcalledwith(99).return_value = "you called me with arg 99"
    
    assertTrue(mockobj.methodfromdepclass(42), "you called me with arg 42")
    assertTrue(mockobj.methodfromdepclass(99), "you called me with arg 99")
    
    cutinst1 = ClassUnderTest(mockobj, 42)
    cutinst2 = ClassUnderTest(mockobj, 99)
    
    # now cutinst1 & cutinst2 contain different values
    

    How do I achieve this "ifcalledwith" kind of semantics?

  • Adam Parkin
    Adam Parkin over 12 years
    Awesome, exactly what I needed. Not a fan of the syntax, but works awesome. Thanks!
  • Conrad.Dean
    Conrad.Dean almost 11 years
    +1: tighter syntax than the if-block pattern with side_effect, maybe return Mock() instead of none so that default behavior is maintained?
  • Conrad.Dean
    Conrad.Dean almost 11 years
    Little terse, but I like it. What if the lambda returned a Mock instance so that the unaccounted behaviors revert back to the regular behavior of the mock library?
  • ThatsAMorais
    ThatsAMorais almost 9 years
    One caveat that I'd like to add here into which I ran when using this solution, which works very well, btw, is that if you intend to have exceptions as side-effects one must raise them, rather than returning them. The Mock library is good about letting you assign an exception to the side_effect and figuring it out, but with this method you have to DIY.
  • Addison
    Addison over 8 years
    Thanks for the suggestion. Will update it to return MagicMock() instead.
  • Arar
    Arar about 2 years
    Love it :) So life saving <3