Jul*_*ich 4 python unit-testing monkeypatching pytest python-3.x
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
Run Code Online (Sandbox Code Playgroud)
将给出以下结果:
====================================================================================================== 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
Run Code Online (Sandbox Code Playgroud)
第一个没有打补丁的测试按预期打印出消息。正如预期的那样,第二个测试是无声的。在第三个测试中,来自构造函数的消息被抑制,但是来自析构函数的消息被打印出来。
这是错误还是功能?我该如何解决这个问题?
有两件事会影响 的嘲讽__del__:
一旦测试功能结束,方法的monkeypatching就会恢复
这在monkeypatch API 参考中提到,也可以从monkeypatchfixture 本身的代码中看出,它调用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() # <----------------
Run Code Online (Sandbox Code Playgroud)
正如在另一个答案中所指出的,__del__被调用的时间(即对象被销毁和垃圾收集时)并不是您可以保证或期望在测试函数引发AssertionError. 只有在没有更多引用时才会调用它:
CPython 实现细节:引用循环可以防止对象的引用计数变为零。在这种情况下,循环稍后将被循环垃圾收集器检测并删除。引用循环的一个常见原因是在局部变量中捕获了异常。帧的局部变量然后引用异常,它引用它自己的回溯,它引用在回溯中捕获的所有帧的局部变量。
考虑到这两件事,发生的事情是在最终删除对象__del__之前(调用其函数时),monkeypatching被撤消或恢复。由于我们在这里处理异常,因此对异常周围局部变量的引用可能仍存储在某处,因此实例的引用计数在其仍被修补时并未变为零。MyClassc__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:
Run Code Online (Sandbox Code Playgroud)
据我所知,函数被调用hook_impl.function(见passed 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
Run Code Online (Sandbox Code Playgroud)
我不确定我上面所做的是否正确,但我认为:
c对象的引用,防止__del__在函数结束时被-edc仍然没有被垃圾收集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
Run Code Online (Sandbox Code Playgroud)
现在为
我该如何解决这个问题?
而不是依赖于对象最终被删除和 on 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
Run Code Online (Sandbox Code Playgroud)
请参阅覆盖析构函数而不调用其父对象。由于派生类不调用父类' __del__,因此在测试期间不会调用它。它类似于用其他方法替换方法的monkeypatching,但这里的定义__del__在整个测试期间保持模拟。的所有其他功能MyClass仍应从MockedMyClass.
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
Run Code Online (Sandbox Code Playgroud)
在这里,我们看到销毁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
Run Code Online (Sandbox Code Playgroud)
| 归档时间: |
|
| 查看次数: |
135 次 |
| 最近记录: |