在下面的最小代码示例中,我展示了两个相同的类。两个版本都可以在提取此摘录的原始源中正常工作。
问题是一条通常有效的 tkinter 错误消息,但似乎是错误生成的。请注意,有很多关于此的问题,所有这些问题都与错误消息的正确产生有关。
其中一个班级
Factory
总是在测试中失败。另一个PostInit
有两个测试;一个有效,另一个失败。这些测试是为 pytest 框架编写的。
失败是tkinter错误:
运行时错误:创建变量太早:没有默认根窗口
通过的测试
test_post_init
嘲笑并阻止了整个tkinter
。当在 test_tk_unmocked
中注释掉模拟时,会产生 tkinter 错误。我认为这证明了错误消息的正确产生。
在总是失败的情况下
test_factory
tkinter 再次被完全嘲笑,但仍然设法从 Tk/Tcl 内部深处产生错误消息。
test_factory
如何设法绕过tkinter
行中对monkeypatch.setattr(patterns_so, "tk", MagicMock())
的嘲笑?
"""patterns_so.py"""
from dataclasses import dataclass, field
import tkinter as tk
@dataclass
class PostInit:
# Passes if tkinter is mocked.
textvariable: tk.StringVar = None
def __post_init__(self):
self.textvariable = tk.StringVar()
@dataclass
class Factory:
# Always fails.
_textvariable: tk.StringVar = field(
default_factory=tk.StringVar, init=False, repr=False
)
"""test_patterns_so.py"""
from unittest.mock import MagicMock
import patterns_so
class TestFacade:
def test_post_init(self, monkeypatch):
# Passes.
monkeypatch.setattr(patterns_so, "tk", MagicMock())
patterns_so.PostInit()
def test_tk_unmocked(self, monkeypatch):
# Expected fail.
# monkeypatch.setattr(patterns_so, "tk", MagicMock())
patterns_so.PostInit()
def test_factory(self, monkeypatch):
# Unexpected fail.
monkeypatch.setattr(patterns_so, "tk", MagicMock())
patterns_so.Factory()
根本问题似乎是数据类模块的机制过早实例化。
在
dataclasses
上文档†的底部附近有一条注释,指出当存在 default_factory 时,init=False
将被忽略,因此它will 会包含在生成的 __init__
中。我通过进一步测试证实了这一点。
天真的我以为
__init__
仅在实例化类时运行。
根据文档 __init__
调用 __post_init__
。我不敢苟同。这里观察到的行为表明,生成的 __init__
函数实际上是在创建类时运行的 而不是稍后 在实例化类时运行。实例化后,__post_init__
函数将运行。
如果模拟旨在阻止启动昂贵或持久的资源,它将失败:资源将被启动。
任何强制延迟模拟属性实例化的方法都可以。 Riccardo Bucco 的 lambda 或我使用
__post_init__
的示例都可以确保成功。
感谢 Riccardo Bucco 指导我对数据类有了更深入的理解。
†文档底部:又名“小字”。
您应该确保
tk.StringVar
不会在类定义时调用,而是在仅在模拟后执行的方法或函数中调用。一种解决方案是使用 lambda 或其他函数来延迟 tk.StringVar
的计算:
@dataclass
class Factory:
_textvariable: tk.StringVar = field(
default_factory=lambda: tk.StringVar(), init=False, repr=False
)