Skip to content Skip to sidebar Skip to footer

Mocking An Api Call Within A Function Based On Inputs

Say one has a function that, among other tasks, makes a few api calls. Is there a way, when testing this function, to mock all the api calls and specify return values from the call

Solution 1:

You mentioned that the behavior of the someApiCall depends on the time argument:

... lets say that time is some value I care about a specific output from someApiCall, I would want to make sure the mock returns that...

To do this, then we have to intercept calls to the outer someFunction and check the time argument so that we can update the someApiCall accordingly. One solution is by decorating someFunction to intercept the calls and modify someApiCall during runtime based on the time argument before calling the original someFunction.

Below is an implementation using decorator. I made 2 possible ways:

  • one by patching via someFunction_decorator_patch
  • and another by manually modifying the source code implementation and then performing a reload via someFunction_decorator_reload

./src.py

from api import someApiCall


def someFunction(time, a, b, c):
    apiReturnA = someApiCall(a)
    returnB = b + 1
    apiReturnC = someApiCall(c)
    return [apiReturnA, returnB, apiReturnC]

./api.py

def someApiCall(var):
    returnvar + 2

./test_src.py

from importlib import reload
import sys

import api
from api import someApiCall
from src import someFunction

import pytest


defamend_someApiCall_yesterday(var):
    # Reimplement api.someApiCallreturn var * 2defamend_someApiCall_now(var):
    # Reimplement api.someApiCallreturn var * 3defamend_someApiCall_later(var):
    # Just wrap around api.someApiCall. Call the original function afterwards. Here we can also put# some conditionals e.g. only call the original someApiCall if the var is an even number.
    var *= 4return someApiCall(var)


defsomeFunction_decorator_patch(someFunction, mocker):
    defwrapper(time, a, b, c):
        # If x imports y.z and we want to patch the calls to z, then we have to patch x.z. Patching# y.z would still retain the original value of x.z thus still calling the original# functionality. Thus here, we would be patching src.someApiCall and not api.someApiCall.if time == "yesterday":
            mocker.patch("src.someApiCall", side_effect=amend_someApiCall_yesterday)
        elif time == "now":
            mocker.patch("src.someApiCall", side_effect=amend_someApiCall_now)
        elif time == "later":
            mocker.patch("src.someApiCall", side_effect=amend_someApiCall_later)
        elif time == "tomorrow":
            mocker.patch("src.someApiCall", return_value=0)
        else:
            # Use the original api.someApiCallpassreturn someFunction(time, a, b, c)
    return wrapper


defsomeFunction_decorator_reload(someFunction):
    defwrapper(time, a, b, c):
        # If x imports y.z and we want to update the functionality of z, then we have to update# first the functionality of z then reload x. This way, x would have the updated# functionality of z.if time == "yesterday":
            api.someApiCall = amend_someApiCall_yesterday
        elif time == "now":
            api.someApiCall = amend_someApiCall_now
        elif time == "later":
            api.someApiCall = amend_someApiCall_later
        elif time == "tomorrow":
            api.someApiCall = lambda var: 0else:
            # Use the original api.someApiCall
            api.someApiCall = someApiCall
        reload(sys.modules['src'])
        return someFunction(time, a, b, c)
    return wrapper


@pytest.mark.parametrize('time',
    [
        'yesterday',
        'now',
        'later',
        'tomorrow',
        'whenever',
    ],
)deftest_sample(time, mocker):
    a, b, c = 10, 10, 10

    someFunction_wrapped_patch = someFunction_decorator_patch(someFunction, mocker)
    result_1 = someFunction_wrapped_patch(time, a, b, c)
    print("Using patch:", time, result_1)

    someFunction_wrapped_reload = someFunction_decorator_reload(someFunction)
    result_2 = someFunction_wrapped_reload(time, a, b, c)
    print("Using reload:", time, result_2)

Output:

$ pytest -rP
____________________________________________________________________________________ test_sample[yesterday] _____________________________________________________________________________________
------------------------------------------------------------------------------------- Captured stdout call --------------------------------------------------------------------------------------
Using patch: yesterday [20, 11, 20]
Using reload: yesterday [20, 11, 20]
_______________________________________________________________________________________ test_sample[now] ________________________________________________________________________________________
------------------------------------------------------------------------------------- Captured stdout call --------------------------------------------------------------------------------------
Using patch: now [30, 11, 30]
Using reload: now [30, 11, 30]
______________________________________________________________________________________ test_sample[later] _______________________________________________________________________________________
------------------------------------------------------------------------------------- Captured stdout call --------------------------------------------------------------------------------------
Using patch: later [42, 11, 42]
Using reload: later [42, 11, 42]
_____________________________________________________________________________________ test_sample[tomorrow] _____________________________________________________________________________________
------------------------------------------------------------------------------------- Captured stdout call --------------------------------------------------------------------------------------
Using patch: tomorrow [0, 11, 0]
Using reload: tomorrow [0, 11, 0]
_____________________________________________________________________________________ test_sample[whenever] _____________________________________________________________________________________
------------------------------------------------------------------------------------- Captured stdout call --------------------------------------------------------------------------------------
Using patch: whenever [12, 11, 12]
Using reload: whenever [12, 11, 12]
======================================================================================= 5 passed in 0.03s =======================================================================================

Here, you can see that the responses from someApiCall changes based on the time argument.

  • yesterday means var * 2
  • now means var * 3
  • later means (var * 4) + 2
  • tomorrow means 0
  • anything else means the default implementation of var + 2

Solution 2:

Let's say that your test file is test.py, and your library file is lib.py. Then test.py should read as follows:

import lib

deffakeApiCall(*args, **kwargs):
  return"fake"

lib.someApiCall = fakeApiCall

lib.someFunction(args)

The someApiCall method is just a variable in the namespace of the module in question. So, change the value of that variable. You may need to dig into locals() and/or globals(), like so:

data = Nonelocals()['data'] = "data"print(data)  # will print "data"

Post a Comment for "Mocking An Api Call Within A Function Based On Inputs"