gpt4 book ai didi

python - 为什么修补析构函数 (__del__) 对失败的测试不起作用?

转载 作者:行者123 更新时间:2023-12-03 21:05:57 28 4
gpt4 key购买 nike

monkeypatch是 pytest 中的一个很棒的工具,允许您替换当前测试范围内的任何函数。最棒的事情之一是甚至可以修补构造函数。然而不幸的是,我在修补析构函数时遇到了麻烦。它似乎只有在测试成功时才有效。如果测试失败,则会调用常规构造函数。考虑这个代码:

class MyClass:
def __init__(self):
print("Constructing MyClass")
def __del__(self):
print("Destroying MyClass")

def test_NoPatch():
c = MyClass()

def test_Patch(monkeypatch, mocker):
monkeypatch.setattr(MyClass, '__init__', mocker.MagicMock(return_value=None))
monkeypatch.setattr(MyClass, '__del__', mocker.MagicMock(return_value=None))
c = MyClass()

def test_PatchWithFailure(monkeypatch, mocker):
monkeypatch.setattr(MyClass, '__init__', mocker.MagicMock(return_value=None))
monkeypatch.setattr(MyClass, '__del__', mocker.MagicMock(return_value=None))
c = MyClass()
assert False
将给出以下结果:
====================================================================================================== test session starts ======================================================================================================
platform linux -- Python 3.8.5, pytest-6.2.2, py-1.10.0, pluggy-0.13.1 -- /home/julian/devel/tests/test_pytest_monkeypatch/testenv/bin/python3
cachedir: .pytest_cache
rootdir: /home/julian/devel/tests/test_pytest_monkeypatch
plugins: mock-3.5.1
collected 3 items

test.py::test_NoPatch Constructing MyClass
Destroying MyClass
PASSED
test.py::test_Patch PASSED
test.py::test_PatchWithFailure FAILED

=========================================================================================================== FAILURES ============================================================================================================
_____________________________________________________________________________________________________ test_PatchWithFailure _____________________________________________________________________________________________________

monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x7f7e94e03490>, mocker = <pytest_mock.plugin.MockerFixture object at 0x7f7e94e222b0>

def test_PatchWithFailure(monkeypatch, mocker):
monkeypatch.setattr(MyClass, '__init__', mocker.MagicMock(return_value=None))
monkeypatch.setattr(MyClass, '__del__', mocker.MagicMock(return_value=None))
c = MyClass()
> assert False
E assert False

test.py:19: AssertionError
==================================================================================================== short test summary info ====================================================================================================
FAILED test.py::test_PatchWithFailure - assert False
================================================================================================== 1 failed, 2 passed in 0.03s ==================================================================================================
Destroying MyClass
第一个没有打补丁的测试会按预期打印出消息。正如预期的那样,第二个测试是无声的。在第三个测试中,来自构造函数的消息被抑制,但是来自析构函数的消息被打印出来。
这是错误还是功能?我该如何解决这个问题?

最佳答案

有两件事会影响 __del__ 的 mock :

  • 一旦测试功能结束,方法的猴子补丁就会恢复
    这是在monkeypatch API reference中提到的从monkeypatch的代码也可以看出 fixture 本身,它调用 MonkeyPatch.undo() 方法:
    @fixture
    def monkeypatch() -> Generator["MonkeyPatch", None, None]:
    """A convenient fixture for monkey-patching.

    ...

    All modifications will be undone after the requesting test function or
    fixture has finished. ...
    """
    mpatch = MonkeyPatch()
    yield mpatch
    mpatch.undo() # <----------------
  • this other answer 中所述,时辰__del__被调用(即当对象被销毁并被垃圾收集时)不是您可以保证或期望在测试函数引发 AssertionError 时发生的事情。 .只有在没有更多引用时才会调用它:

    CPython implementation detail: It is possible for a reference cycle to prevent the reference count of an object from going to zero. In this case, the cycle will be later detected and deleted by the cyclic garbage collector. A common cause of reference cycles is when an exception has been caught in a local variable. The frame’s locals then reference the exception, which references its own traceback, which references the locals of all frames caught in the traceback.



  • 考虑到这两件事,会发生什么是 __del__的monkeypatching在 MyClass 之前被撤消或恢复对象 c最终被删除(当它的 __del__ 函数被调用时)。由于我们在这里处理一个异常,很可能对异常周围的局部变量的引用仍然存储在某处,因此 c 的引用计数。实例没有变为零,而它的 __del__仍然被修补。
    我试图通过使用 --full-trace 运行测试来验证这一点。和 --showlocals选项。您将在名为 _multicall 的函数中看到这一点。 ,运行测试函数并捕获异常:
    $ pytest tests/1.py --setup-show --full-trace --showlocals

    # ...lots of logs...

    hook_impls = [<HookImpl plugin_name='python', plugin=<module '_pytest.python' from '/path/to//lib/python3.8/site-packages/_pytest/python.py'>>]
    caller_kwargs = {'pyfuncitem': <Function test_PatchWithFailure>}, firstresult = True

    def _multicall(hook_impls, caller_kwargs, firstresult=False):
    # ...other parts of function...
    else:
    res = hook_impl.function(*args)
    if res is not None:
    results.append(res)
    if firstresult: # halt further impl calls
    break
    except BaseException:
    excinfo = sys.exc_info()
    finally:
    # ...other parts of function...

    > return outcome.get_result()

    args = [<Function test_PatchWithFailure>]
    caller_kwargs = {'pyfuncitem': <Function test_PatchWithFailure>}
    excinfo = (<class 'AssertionError'>, AssertionError('assert False'), <traceback object at 0x1108ea800>)
    firstresult = True

    # ...lots of logs...

    ../../path/to/lib/python3.8/site-packages/pluggy/callers.py:197:
    据我所知,该函数是在 hook_impl.function 中调用的。 (见通过 args),然后是 assert False发生在 except 中。块,异常信息存储在 excinfo .此异常信息然后存储对 c 的引用。实例在其 traceback object .
    # In test_PatchWithFailure
    print('>>>> NOW IN test_PatchWithFailure')
    c = MyClass()
    print(c)
    # Logs:
    # <1.MyClass object at 0x10de152e0> # <---- SAME as BELOW

    # In _multicall
    except BaseException:
    print('>>>> NOW IN _multicall')
    excinfo = sys.exc_info()
    import inspect
    # print(inspect.trace()[-1]) # The last entry is where the exception was raised
    # print(inspect.trace()[-1][0]) # The frame object
    # print(inspect.trace()[-1][0].f_locals) # local vars
    print(f'{inspect.trace()[-1].lineno}, {inspect.trace()[-1].code_context}')
    print(f'Is "c" in here?: {"c" in inspect.trace()[-1][0].f_locals}')
    print(inspect.trace()[-1][0].f_locals['c'])
    # Logs:
    # 42, [' assert False\n']
    # Is "c" in here?: True
    # <1.MyClass object at 0x10de152e0> # <---- SAME as ABOVE
    我不确定我上面所做的是否正确,但我认为:
  • 当 pytest 捕获到 AssertionError 时,它有效地添加了对本地 c 的引用。对象,防止它成为 __del__ -ed 当函数结束时
  • 然后在 c 时,monkeypatching 被撤消/恢复仍未被垃圾收集
  • 然后pytest在所有测试函数运行后最终报告它收集的所有错误(释放对c实例的引用,使其__del__ -able)
  • 然后终于c__del__ -ed,但是monkeypatch 已经不见了

  • ----------------------------------------------------------------- Captured stdout call ------------------------------------------------------------------
    >>>> NOW IN test_PatchWithFailure
    <1.MyClass object at 0x10de152e0>

    >>>> NOW IN _multicall
    42, [' assert False\n']
    Is "c" in here?: True
    <1.MyClass object at 0x10de152e0>
    >>>> NOW IN _multicall
    42, [' assert False\n']
    Is "c" in here?: True
    <1.MyClass object at 0x10de152e0>
    --------------------------------------------------------------- Captured stdout teardown ----------------------------------------------------------------
    >>>> NOW returned from yield MonkeyPatch, calling undo()
    >>>> UNDOING <class '1.MyClass'> __del__ <function MyClass.__del__ at 0x10bf04e50>
    >>>> UNDOING <class '1.MyClass'> __init__ <function MyClass.__init__ at 0x10bf04d30>
    ================================================================ short test summary info ================================================================
    FAILED tests/1.py::test_PatchWithFailure - assert False
    =================================================================== 1 failed in 0.16s ===================================================================
    Destroying MyClass

    现在为

    How could I work around this issue?


    而不是依赖于最终删除对象的时间和 monkeypatch -ing __del__ ,一种解决方法是子类化 MyClass相反,然后完全覆盖/替换 __init____del__ :
    def test_PatchWithFailure():
    class MockedMyClass(MyClass):
    def __init__(self):
    print('Calling mocked __init__')
    super().__init__()
    def __del__(self):
    print('Calling mocked __del__')

    c = MockedMyClass()
    assert False
    Overriding destructors without calling their parents .因为,派生类不调用父类' __del__ ,它不会在测试期间被调用。它类似于monkeypatching,将方法替换为其他东西,但这里是 __del__的定义。在整个测试期间保持模拟。 MyClass 的所有其他功能从 MockedMyClass 开始应该仍然可用/可测试.
            c = MockedMyClass()
    > assert False
    E assert False

    tests/1.py:59: AssertionError
    ----------------------------------------------------------------- Captured stdout call ------------------------------------------------------------------
    Calling mocked __init__
    Constructing MyClass
    ================================================================ short test summary info ================================================================
    FAILED tests/1.py::test_PatchWithFailure - assert False
    =================================================================== 1 failed in 0.13s ===================================================================
    Calling mocked __del__
    在这里,我们看到破坏 c只调用模拟 __del__ (这里实际上什么也没做)。不再有“Destroying MyClass”,它有望解决您的问题。创建一个提供 MockedMyClass 的装置应该很简单。实例。
    @pytest.fixture
    def mocked_myclass():
    class MockedMyClass(MyClass):
    def __init__(self):
    print('Calling mocked __init__')
    super().__init__()
    def __del__(self):
    print('Calling mocked __del__')

    return MockedMyClass()

    def test_PatchWithFailure(mocked_myclass):
    c = mocked_myclass
    assert False

    关于python - 为什么修补析构函数 (__del__) 对失败的测试不起作用?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/66644872/

    28 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com