Search code examples
pythonmockingpytestside-effects

Pytest: Mock multiple calls of same method with different side_effect


I have a unit test like so below:

# utilities.py  
def get_side_effects():
    def side_effect_func3(self):
        # Need the "self" to do some stuff at run time.
        return {"final":"some3"} 

    def side_effect_func2(self):
        # Need the "self" to do some stuff at run time.
        return {"status":"some2"}
      
    def side_effect_func1(self):
        # Need the "self" to do some stuff at run time.
        return {"name":"some1"} 

    return side_effect_func1, side_effect_func2, side_effect_func2

#################

# test_a.py
def test_endtoend():
   
    s1, s2, s3 = utilities.get_side_effects()
    
    m1 = mock.MagicMock()
    m1.side_effect = s1

    m2 = mock.MagicMock()
    m2.side_effect = s2

    m3 = mock.MagicMock()
    m3.side_effect = s3
   
    with mock.patch("a.get_request", m3):
        with mock.patch("a.get_request", m2):
            with mock.patch("a.get_request", m1):
                foo = a() # Class to test
                result = foo.run() 
    
    

As part of the foo.run() code run, get_request is called multiple times. I want to have a different side_effect function for each call of get_request method, in this case it is side_effect_func1, side_effect_func2, side_effect_func3. But what I'm noticing is that only m1 mock object is active, i.e only side_effect_func1 is invoked but not the other 2. How do I achieve this?

I have also tried the below, but the actual side_effect functions don't get invoked, they always return the function object, but don't actually execute the side_effect functions.

# utilities.py
def get_side_effects():
    def side_effect_func3(self):
        # Need the "self" to do some stuff at run time.
        return {"final":"some3"} 

    def side_effect_func2(self):
        # Need the "self" to do some stuff at run time.
        return {"status":"some2"}
      
    def side_effect_func1(self):
        # Need the "self" to do some stuff at run time.
        return {"name":"some1"} 

    all_get_side_effects = []
    all_get_side_effects.append(side_effect_func1)
    all_get_side_effects.append(side_effect_func2)
    all_get_side_effects.append(side_effect_func3)
     
    return all_get_side_effects

#########################
# test_a.py
def test_endtoend():

    all_side_effects = utilities.get_side_effects()

    m = mock.MagicMock()
    m.side_effect = all_side_effects

    with mock.patch("a.get_request", m):
       foo = a() # Class to test
       result = foo.run()

Solution

  • Your first attempt doesn't work because each mock just replaced the previous one (the outer two mocks don't do anything).

    Your second attempt doesn't work because side-effect is overloaded to serve a different purpose for iterables (docs):

    If side_effect is an iterable then each call to the mock will return the next value from the iterable.

    Instead you could use a callable class for the side-effect, which is maintaining some state about which underlying function to actually call, consecutively.

    Basic example with two functions:

    >>> class SideEffect:
    ...     def __init__(self, *fns):
    ...         self.fs = iter(fns)
    ...     def __call__(self, *args, **kwargs):
    ...         f = next(self.fs)
    ...         return f(*args, **kwargs)
    ... 
    >>> def sf1():
    ...     print("called sf1")
    ...     return 1
    ... 
    >>> def sf2():
    ...     print("called sf2")
    ...     return 2
    ... 
    >>> def foo():
    ...     print("called actual func")
    ...     return "f"
    ... 
    >>> with mock.patch("__main__.foo", side_effect=SideEffect(sf1, sf2)) as m:
    ...     first = foo()
    ...     second = foo()
    ... 
    called sf1
    called sf2
    >>> assert first == 1
    >>> assert second == 2
    >>> assert m.call_count == 2