根据输入模拟函数内的 API 调用

Mocking an API call within a function based on inputs

假设有一个函数,除其他任务外,还进行了几次 api 调用。有没有办法在测试此功能时模拟所有 api 调用并根据输入从调用中指定 return 值。例如,假设您要测试的函数是这样的:

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

我想测试 someFunction 并指定,每次调用 someApiCall 时,不执行函数,只是 return 基于此函数输入的值。例如,如果我正在处理时间,我希望 api 调用基于特定时间的 return 特定值,否则 return 一个 noop 值。如何做到这一点?

假设您的测试文件是 test.py,您的库文件是 lib.py。那么test.py应该是这样的:

import lib

def fakeApiCall(*args, **kwargs):
  return "fake"

lib.someApiCall = fakeApiCall

lib.someFunction(args)

someApiCall 方法只是相关模块命名空间中的一个变量。因此,更改该变量的值。您可能需要深入研究 locals() and/or globals(),例如:

data = None
locals()['data'] = "data"
print(data)  # will print "data"

您提到 someApiCall 的行为取决于 time 参数:

... 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...

为此,我们必须拦截对外部 someFunction 的调用并检查 time 参数,以便我们可以相应地更新 someApiCall。一种解决方案是在调用原始 someFunction.

之前根据 time 参数装饰 someFunction 以拦截调用并在运行时修改 someApiCall

下面是一个使用装饰器的实现。我做了 2 种可能的方法:

  • 通过 someFunction_decorator_patch
  • 修补一个
  • 和另一个通过手动修改源代码实现,然后通过 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):
    return var + 2

./test_src.py

from importlib import reload
import sys

import api
from api import someApiCall
from src import someFunction

import pytest


def amend_someApiCall_yesterday(var):
    # Reimplement api.someApiCall
    return var * 2


def amend_someApiCall_now(var):
    # Reimplement api.someApiCall
    return var * 3


def amend_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 *= 4
    return someApiCall(var)


def someFunction_decorator_patch(someFunction, mocker):
    def wrapper(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.someApiCall
            pass
        return someFunction(time, a, b, c)
    return wrapper


def someFunction_decorator_reload(someFunction):
    def wrapper(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: 0
        else:
            # 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',
    ],
)
def test_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)

输出:

$ 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 =======================================================================================

在这里,您可以看到来自 someApiCall 的响应根据 time 参数发生变化。

  • yesterday 表示 var * 2
  • now 表示 var * 3
  • later 表示 (var * 4) + 2
  • tomorrow 表示 0
  • 任何其他表示 var + 2 的默认实现