In the following minimal code samples I show what should be two identical classes. Both versions work correctly in the original source from which this extract has been drawn.
The problem is a normally valid tkinter error message that appears to be wrongly generated. Please note there are many SO questions about this all of which relate to the correct production of the error message.
One of the classes Factory always fails on test. The other PostInit has two tests; one works and the other fails. The tests were written for the pytest framework.
The failure is the tkinter error:
RuntimeError: Too early to create variable: no default root window
The passing test test_post_init mocks and blocks the whole of tkinter. When the mock is commented out in test_tk_unmocked the tkinter error is produced. I assume this demonstrates the correct production of the error message.
In the always failing test_factory tkinter is again completely mocked but still manages to produce an error message from deep inside Tk/Tcl.
How is test_factory managing to bypass the mocking of tkinter in the line 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()
Postscript
The fundamental problem appears to be premature instantiation by the mechanics of the dataclasses module.
Very near the bottom of the docs† on dataclasses there is a note which says init=False will be ignored when a default_factory is present and so it will be included in the generated __init__. I confirmed this with further testing.
Naïve me thought that __init__ only got run when the class was instantiated.
According to the docs __init__ calls __post_init__. I beg to differ. The observed behavior here suggests that the generated __init__ function is actually run when the class is created not later when it is instantiated. After instantiation the __post_init__ function will run.
In cases where the mock is intended to prevent an expensive or persistent resource from being started it will fail: The resource will be started.
Any method which forcibly delays instantiation of mocked attributes will work. Either Riccardo Bucco's lambda or my example using __post_init__ ensure success.
Thanks to Riccardo Bucco for guiding me to this deeper understanding of dataclasses.
†bottom of the docs: aka ‘The small print’.
You should ensure
tk.StringVaris not called upon class definition but is instead called within a method or function that is only executed after mocking. One solution is to use a lambda or another function to delay the evaluation oftk.StringVar: