模拟 API 调用嵌入到某些对象中并根据对象内的输入更改行为

Mocking API call embedded in some objects and changing behavior based on inputs within object

这是 提出的 SO 问题的延续,但比最初要求的模式更复杂。我的主要目的是尝试根据传递给调用者的值模拟 API 调用。 API 调用不知道传递给其调用者的值,但需要提供正确的行为以便可以对调用者进行全面测试。我正在使用时间来确定我想要的行为。

给定一个对象:

# some_object.py 
from some_import import someApiCall

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

由另一个具有入口点代码的对象创建:

# some_main.py
import some_object

class myMainObject():
    def entry_point(self, time):
        someObj = some_object.SomeObject()
        if 'yesterday' == time:
            (a, b, c) = (1, 1, 1)
        elif 'today' == time:
            (a, b, c) = (2, 2, 2)
        elif 'later' == time:
            (a, b, c) = (3, 3, 3)
        elif 'tomorrow' == time:
            (a, b, c) = (4, 4, 4)
        else:
            return "ERROR"
        return someObj.someFunction(time, a, b, c)

如何让 someApiCall 根据 time 参数进行更改?

# some_import.py
def someApiCall(var):
    print("I'm not mocked! I'm slow, random and hard to test")
    return var + 2

这是一个示例测试用例

# test_example.py
import some_main
def amend_someApiCall_yesterday(var):
    # Reimplement api.someApiCall
    return var * 2


def amend_someApiCall_today(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.
    import some_import
    var *= 4
    return some_import.someApiCall(var)


def someObject_decorator_patch(someFunction, mocker, *args):
    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("some_object.someApiCall", side_effect=amend_someApiCall_yesterday)
        elif time == "today":
            mocker.patch("some_object.someApiCall", side_effect=amend_someApiCall_today)
        elif time == "later":
            mocker.patch("some_object.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 test_some_main(mocker):
    results = 0
    uut = some_main.myMainObject()
    times = ['yesterday', 'today', 'later', 'tomorrow']
    mocker.patch.object(some_main.some_object.SomeObject, 'someFunction', someObject_decorator_patch)
    for time in times:
        results = uut.entry_point(time)
    print(results)
    assert 0 != results

测试用例没有得到我想要的结果(它returns一个函数指针)。

这是 的临时解决方案,没有使用装饰器。

正如已经指出的那样,我们仍然需要拦截对接受 time 参数的函数的调用,该参数指示 someApiCall 的行为方式,即 entry_pointsomeFunction。在这里我们将拦截 someFunction.

而不是在 someFunction 上实现 python 装饰器,然后需要调用显式创建的装饰函数,在这里我们将修改(这仍然遵循装饰器设计模式) someFunction 就地并使其可用于其余的源代码调用,而无需显式更改对装饰函数的调用。这就像原始功能的就地替换,我们将用更新的功能替换(或更确切地说环绕)原始功能,该功能将在调用原始功能之前对 time 进行评估。

也供你参考,我解决了2种类型的函数,一个class方法src.SomeClass.someFunction和一个全局函数src.someFunction2

./_main.py

import src

class MyMainClass:
    def __init__(self):
        self.var = 0

    def entry_point(self, time):
        someObj = src.SomeClass()
        self.var += 1
        if self.var >= 10:
            self.var = 0
        ret =  f'\n[1]entry_point({time})-->{someObj.someFunction(time, self.var)}'
        self.var += 1
        ret +=  f'\n[2]entry_point({time})-->{src.someFunction2(time, self.var)}'
        return ret

./src.py


class SomeClass:
    def someFunction(self, time, var):
        return f'someFunction({time},{var})-->{someSloowApiCall(var)}'

def someFunction2(time, var):
    return f'someFunction2({time},{var})-->{someSloowApiCall2(var)}'

./api.py

def someSloowApiCall(var):
    return f'someSloowApiCall({var})-->{special_message(var)}'

def someSloowApiCall2(var):
    return f'someSloowApiCall2({var})-->{special_message(var)}'

def special_message(var):
    special_message = "I'm not mocked! I'm slow, random and hard to test"
    if var > 10:
        special_message = "I'm mocked! I'm not slow, random or hard to test"
    return special_message

./test_main.py

import _main, pytest, api


def amend_someApiCall_yesterday(var):
    # Reimplement api.someSloowApiCall
    return f'amend_someApiCall_yesterday({var})'


def amend_someApiCall_today(var):
    # Reimplement api.someSloowApiCall
    return f'amend_someApiCall_today({var})'


def amend_someApiCall_later(var):
    # Just wrap around api.someSloowApiCall. Call the original function afterwards. Here we can also put
    # some conditionals e.g. only call the original someSloowApiCall if the var is an even number.
    return f'amend_someApiCall_later({var})-->{api.someSloowApiCall(var+10)}'

def amend_someApiCall_later2(var):
    # Just wrap around api.someSloowApiCall2. Call the original function afterwards. Here we can also put
    # some conditionals e.g. only call the original someSloowApiCall2 if the var is an even number.
    return f'amend_someApiCall_later2({var})-->{api.someSloowApiCall2(var+10)}'


def get_amended_someFunction(mocker, original_func):
    def amend_someFunction(self, time, var):
        if time == "yesterday":
            mocker.patch("_main.src.someSloowApiCall", amend_someApiCall_yesterday)
            # or
            # src.someSloowApiCall = amend_someApiCall_yesterday
        elif time == "today":
            mocker.patch("_main.src.someSloowApiCall", amend_someApiCall_today)
            # or
            # src.someSloowApiCall = amend_someApiCall_today
        elif time == "later":
            mocker.patch("_main.src.someSloowApiCall", amend_someApiCall_later)
            # or
            # src.someSloowApiCall = amend_someApiCall_later
        elif time == "tomorrow":
            mocker.patch("_main.src.someSloowApiCall", lambda var: f'lambda({var})')
            # or
            # src.someSloowApiCall = lambda var: 0
        else:
            pass
            # or
            # src.someSloowApiCall = someSloowApiCall
        return original_func(self, time, var)
    return amend_someFunction


def get_amended_someFunction2(mocker, original_func):
    def amend_someFunction2(time, var):
        if time == "yesterday":
            mocker.patch("_main.src.someSloowApiCall2", amend_someApiCall_yesterday)
            # or
            # src.someSloowApiCall2 = amend_someApiCall_yesterday
        elif time == "today":
            mocker.patch("_main.src.someSloowApiCall2", amend_someApiCall_today)
            # or
            # src.someSloowApiCall2 = amend_someApiCall_today
        elif time == "later":
            mocker.patch("_main.src.someSloowApiCall2", amend_someApiCall_later2)
            # or
            # src.someSloowApiCall2 = amend_someApiCall_later
        elif time == "tomorrow":
            mocker.patch("_main.src.someSloowApiCall2", lambda var : f'lambda2({var})')
            # or
            # src.someSloowApiCall2 = lambda var: 0
        else:
            pass
            # or
            # src.someSloowApiCall2 = someSloowApiCall2
        return original_func(time, var)
    return amend_someFunction2


@pytest.mark.parametrize(
    'time',
    [
        'yesterday',
        'today',
        'later',
        'tomorrow',
        'whenever',
    ],
)
def test_entrypointFunction(time, mocker):
    mocker.patch.object(
        _main.src.SomeClass,
        "someFunction",
        side_effect=get_amended_someFunction(mocker, _main.src.SomeClass.someFunction),
        autospec=True,  # Needed for the self argument
    )
    # or
    # src.SomeClass.someFunction = get_amended_someFunction(mocker, src.SomeClass.someFunction)

    mocker.patch(
        "_main.src.someFunction2",
        side_effect=get_amended_someFunction2(mocker, _main.src.someFunction2),
    )
    # or
    # src.someFunction2 = get_amended_someFunction2(mocker, src.someFunction2)

    uut = _main.MyMainClass()
    print(f'\nuut.entry_point({time})-->{uut.entry_point(time)}')

输出:

    $ pytest -rP
    =================================== PASSES ====================================
_____________________ test_entrypointFunction[yesterday] ______________________
---------------------------- Captured stdout call -----------------------------

uut.entry_point(yesterday)-->
[1]entry_point(yesterday)-->someFunction(yesterday,1)-->amend_someApiCall_yesterday(1)
[2]entry_point(yesterday)-->someFunction2(yesterday,2)-->amend_someApiCall_yesterday(2)
_______________________ test_entrypointFunction[today] ________________________
---------------------------- Captured stdout call -----------------------------

uut.entry_point(today)-->
[1]entry_point(today)-->someFunction(today,1)-->amend_someApiCall_today(1)
[2]entry_point(today)-->someFunction2(today,2)-->amend_someApiCall_today(2)
_______________________ test_entrypointFunction[later] ________________________
---------------------------- Captured stdout call -----------------------------

uut.entry_point(later)-->
[1]entry_point(later)-->someFunction(later,1)-->amend_someApiCall_later(1)-->someSloowApiCall(11)-->I'm mocked! I'm not slow, random or hard to test
[2]entry_point(later)-->someFunction2(later,2)-->amend_someApiCall_later2(2)-->someSloowApiCall2(12)-->I'm mocked! I'm not slow, random or hard to test
______________________ test_entrypointFunction[tomorrow] ______________________
---------------------------- Captured stdout call -----------------------------

uut.entry_point(tomorrow)-->
[1]entry_point(tomorrow)-->someFunction(tomorrow,1)-->lambda(1)
[2]entry_point(tomorrow)-->someFunction2(tomorrow,2)-->lambda2(2)
______________________ test_entrypointFunction[whenever] ______________________
---------------------------- Captured stdout call -----------------------------

uut.entry_point(whenever)-->
[1]entry_point(whenever)-->someFunction(whenever,1)-->someSloowApiCall(1)-->I'm not mocked! I'm slow, random and hard to test
[2]entry_point(whenever)-->someFunction2(whenever,2)-->someSloowApiCall2(2)-->I'm not mocked! I'm slow, random and hard to test
============================== 5 passed in 0.07s ==============================