【问题标题】:Mocking an API call within a function based on inputs根据输入在函数内模拟 API 调用
【发布时间】:2021-05-12 05:36:42
【问题描述】:

假设有一个函数,除其他任务外,还进行了一些 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 值。怎么可能做到这一点?

【问题讨论】:

  • 在 someFunction 中,编写一个 override_function,在调用时将 someApiCall 映射到 userDefinedSomeApiCall?
  • 只需编写您自己的 someApiCall 测试版本即可满足您的需求,并使用该版本模拟实际功能(例如 patch('someModule.someApiCall', myApiCall))。
  • 路径没有解决的问题(或者我看不到如何解决)是可以说时间是一些价值,我关心 someApiCall 的特定输出,我想确保模拟返回...否则,我希望它返回默认的 noop 值。我不知道如何根据时间参数改变 someApiCall 的行为。
  • 这是你的函数,你可以写任何returnj值。 someApiCall 没有被调用。
  • @LeanMan 我只是想澄清一下,时间只是 someFunction() 的一个参数,对吧?它真的不是 someApiCall() 的论据吗?

标签: python pytest-mock


【解决方案1】:

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

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

为此,我们必须拦截对外部someFunction 的调用并检查time 参数,以便我们可以相应地更新someApiCall。一种解决方案是通过装饰someFunction 来拦截调用并在运行时根据time 参数修改someApiCall,然后再调用原始someFunction

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

  • 通过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 的默认实现

【讨论】:

  • 我认为这与我正在寻找的内容很接近,但测试确实需要调用装饰器才能测试 someFunction()。如果这是不可能的怎么办?比如 someFunction() 是一个对象中的函数,你只想在它调用 someFunction() 时修改该对象对 someApiCall() 的调用(基于时间参数)?
  • 你的意思是 someFunction() 是一个类的方法,例如我的课?它将从同一类的另一个方法内部调用,例如MyClass.entrypointFunction()?像这样的东西? class MyClass: def someFunction(self, time, a, b, c): apiReturnA = someApiCall(a) returnB = b + 1 apiReturnC = someApiCall(c) return [apiReturnA, returnB, apiReturnC] def someOtherFunction(self): return self .someFunction("现在", 10, 10, 10)
  • 这是一个场景,除了它是一个实例而不是一个类。让我们称之为myObject。另一种情况是另一个对象调用myObject.someFunction(time, a, b, c)。让我们称这个另一个对象为entryPointObj,现在我们有了被测单元results = entryPointObj.behavior() -> myObject.someFunction(time, a, b, c) -> 拦截时间并修改api.someApiCall -> 执行myObject.someFunction(time, a, b, c) -> 返回结果。在entryPointObj.behavior() 完成后,测试results 的预期行为。
  • 我使用您的部分答案创建了一个新问题,以帮助改进我所追求的场景。谢谢您的帮助! stackoverflow.com/questions/67505679/…
  • 好的。我实际上是在让它工作:D 我会在新线程上回复 :)
【解决方案2】:

假设你的测试文件是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"

【讨论】:

  • 为什么不改用标准且不易出错的mock.patch
  • 因为我直到现在才遇到它——谢谢你的教训:)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2020-03-23
  • 1970-01-01
  • 2014-03-04
  • 2021-10-26
  • 2014-05-29
  • 1970-01-01
  • 2018-10-01
相关资源
最近更新 更多