【问题标题】:Mocking API call embedded in some objects and changing behavior based on inputs within object模拟嵌入在某些对象中的 API 调用并根据对象内的输入改变行为
【发布时间】:2021-05-12 14:31:22
【问题描述】:

这是here 提出的 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

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

【问题讨论】:

    标签: python pytest-mock


    【解决方案1】:

    这是https://stackoverflow.com/a/67498948/11043825 的临时解决方案,不使用装饰器。

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

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

    也供您参考,我解决了两种类型的函数,一个类方法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 ==============================
    

    【讨论】:

    • 我对您的解决方案进行了编辑,但建议的编辑队列似乎已满。一旦队列清理完毕,我会添加我的建议。
    • 我已经更新了你的答案。它显示了通过模拟发生的一系列调用,所以我认为这更好。很好!让我知道你是否有任何 cmets。一旦接受编辑,我将把它作为问题的答案。
    • 已接受。很高兴它帮助了你:)
    猜你喜欢
    • 1970-01-01
    • 2021-10-26
    • 1970-01-01
    • 2023-03-18
    • 2015-08-13
    • 1970-01-01
    • 2021-08-26
    • 1970-01-01
    • 2016-05-27
    相关资源
    最近更新 更多