0

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

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

我想测试 someFunction 并指定,每次调用 someApiCall 时,不要执行该函数,只需根据该函数的输入返回一个值。例如,如果我正在处理时间,我希望 api 调用根据特定时间返回特定值,否则返回 noop 值。怎么能做到这一点?

4

2 回答 2

1

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

...可以说时间是我关心someApiCall的特定输出的一些价值,我想确保模拟返回...

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

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

  • 一个是通过修补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 的默认实现
于 2021-05-12T07:20:43.373 回答
0

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

import lib

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

lib.someApiCall = fakeApiCall

lib.someFunction(args)

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

data = None
locals()['data'] = "data"
print(data)  # will print "data"
于 2021-05-12T05:51:44.420 回答