为何dataclasses中使用default_factory时,tkinter的MonkeyPatch Mock会失效?
你碰到的这个问题其实是dataclasses模块的底层机制在“搞鬼”,咱们一步步拆解来看清楚。
首先还原你的代码场景:
业务代码(patterns_so.py)
"""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)
"""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()
现象与疑惑
你会发现:
test_post_init在mock tkinter后能正常通过test_tk_unmocked没mock时,会抛出预期的RuntimeError: Too early to create variable: no default root window错误- 但
test_factory明明也mock了tkinter,却还是触发了这个错误,这就很让人困惑了
核心原因:dataclasses的字段处理时机
这个问题的根源在于dataclasses对default_factory的处理时机和你预期的不一样:
你原本以为只有当类被实例化时,__init__才会运行,但实际上,当你用@dataclass装饰器定义类的时候,Python解释器加载这个类的阶段,dataclasses模块就会提前解析field里的default_factory参数——它会直接绑定到当时的tk.StringVar对象,而不是等到你调用patterns_so.Factory()的时候才去获取patterns_so.tk指向的内容。
也就是说,当Factory类被定义的那一刻,default_factory=tk.StringVar就已经和真实的tkinter库绑定了,你在测试里用monkeypatch替换patterns_so.tk的操作已经太晚了,根本影响不到已经绑定好的default_factory。
而PostInit类的情况不同:它是在__post_init__方法里才调用tk.StringVar(),__post_init__是在类实例化的时候才执行的,这时候你的monkeypatch已经生效,所以mock能正常工作。
另外你提到的dataclasses文档里的细节——当default_factory存在时,init=False会被忽略——这也是在类定义阶段就被处理的逻辑,进一步验证了类定义时dataclasses就会完成这些字段参数的解析工作。
解决方案:延迟引用
要解决这个问题,关键是要延迟default_factory对tk.StringVar的引用,让它在实例化的时候才去获取当前的tk对象。最简单的办法就是用lambda函数包裹一下:
@dataclass class Factory: _textvariable: tk.StringVar = field( default_factory=lambda: tk.StringVar(), init=False, repr=False )
这样一来,default_factory指向的是一个lambda函数,这个函数只有在实例化类的时候才会被调用,这时候你的monkeypatch已经替换了patterns_so.tk,自然会调用mock后的StringVar,也就不会触发错误了。
备注:内容来源于stack exchange,提问作者lemi57ssss

