Skip to content

Commit 912288b

Browse files
committed
tests(textframe): Replace patch() with monkeypatch.setattr()
why: Follow pytest best practices from CLAUDE.md guidelines what: - Use import unittest.mock namespace style - Replace patch() context managers with monkeypatch.setattr() - Document MagicMock necessity for curses window simulation
1 parent 50a6f97 commit 912288b

File tree

1 file changed

+42
-32
lines changed

1 file changed

+42
-32
lines changed

tests/textframe/test_display.py

Lines changed: 42 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
1-
"""Tests for TextFrame.display() interactive viewer."""
1+
"""Tests for TextFrame.display() interactive viewer.
2+
3+
Note on MagicMock usage: These tests require MagicMock to create mock curses.window
4+
objects with configurable return values and side effects. pytest's monkeypatch
5+
fixture patches existing attributes but doesn't create mock objects with the
6+
call tracking and behavior configuration needed for curses window simulation.
7+
"""
28

39
from __future__ import annotations
410

511
import curses
612
import io
713
import os
14+
import sys
815
import typing as t
9-
from unittest.mock import MagicMock, patch
16+
import unittest.mock
1017

1118
import pytest
1219

@@ -39,40 +46,38 @@ class ExitKeyCase(t.NamedTuple):
3946

4047

4148
@pytest.fixture
42-
def mock_curses_env() -> t.Generator[None, None, None]:
49+
def mock_curses_env(monkeypatch: pytest.MonkeyPatch) -> None:
4350
"""Mock curses module-level functions that require initscr()."""
44-
with (
45-
patch("curses.curs_set"),
46-
patch("curses.A_REVERSE", 0),
47-
):
48-
yield
51+
monkeypatch.setattr(curses, "curs_set", lambda x: None)
52+
monkeypatch.setattr(curses, "A_REVERSE", 0)
4953

5054

51-
def test_display_raises_when_not_tty() -> None:
55+
def test_display_raises_when_not_tty(monkeypatch: pytest.MonkeyPatch) -> None:
5256
"""Verify display() raises RuntimeError when stdout is not a TTY."""
5357
frame = TextFrame(content_width=10, content_height=2)
5458
frame.set_content(["hello", "world"])
5559

56-
with (
57-
patch("sys.stdout", new=io.StringIO()),
58-
pytest.raises(RuntimeError, match="interactive terminal"),
59-
):
60+
monkeypatch.setattr(sys, "stdout", io.StringIO())
61+
62+
with pytest.raises(RuntimeError, match="interactive terminal"):
6063
frame.display()
6164

6265

63-
def test_display_calls_curses_wrapper_when_tty() -> None:
66+
def test_display_calls_curses_wrapper_when_tty(
67+
monkeypatch: pytest.MonkeyPatch,
68+
) -> None:
6469
"""Verify display() calls curses.wrapper when stdout is a TTY."""
6570
frame = TextFrame(content_width=10, content_height=2)
6671
frame.set_content(["hello", "world"])
6772

68-
with (
69-
patch("sys.stdout.isatty", return_value=True),
70-
patch("curses.wrapper") as mock_wrapper,
71-
):
72-
frame.display()
73-
mock_wrapper.assert_called_once()
74-
args = mock_wrapper.call_args[0]
75-
assert args[0].__name__ == "_curses_display"
73+
monkeypatch.setattr("sys.stdout.isatty", lambda: True)
74+
mock_wrapper = unittest.mock.MagicMock()
75+
monkeypatch.setattr(curses, "wrapper", mock_wrapper)
76+
77+
frame.display()
78+
mock_wrapper.assert_called_once()
79+
args = mock_wrapper.call_args[0]
80+
assert args[0].__name__ == "_curses_display"
7681

7782

7883
@pytest.mark.parametrize("case", EXIT_KEY_CASES, ids=lambda c: c.id)
@@ -84,7 +89,7 @@ def test_curses_display_exit_keys(
8489
frame = TextFrame(content_width=10, content_height=2)
8590
frame.set_content(["hello", "world"])
8691

87-
mock_stdscr = MagicMock()
92+
mock_stdscr = unittest.mock.MagicMock()
8893

8994
if case.side_effect:
9095
mock_stdscr.getch.side_effect = case.side_effect
@@ -101,7 +106,7 @@ def test_curses_display_scroll_navigation(mock_curses_env: None) -> None:
101106
frame = TextFrame(content_width=10, content_height=10)
102107
frame.set_content([f"line {i}" for i in range(10)])
103108

104-
mock_stdscr = MagicMock()
109+
mock_stdscr = unittest.mock.MagicMock()
105110

106111
# Simulate: down arrow, then quit
107112
mock_stdscr.getch.side_effect = [curses.KEY_DOWN, ord("q")]
@@ -117,7 +122,7 @@ def test_curses_display_status_line(mock_curses_env: None) -> None:
117122
frame = TextFrame(content_width=10, content_height=2)
118123
frame.set_content(["hello", "world"])
119124

120-
mock_stdscr = MagicMock()
125+
mock_stdscr = unittest.mock.MagicMock()
121126
mock_stdscr.getch.return_value = ord("q")
122127

123128
frame._curses_display(mock_stdscr)
@@ -131,7 +136,10 @@ def test_curses_display_status_line(mock_curses_env: None) -> None:
131136
assert len(status_calls) > 0, "Status line should be displayed"
132137

133138

134-
def test_curses_display_uses_shutil_terminal_size(mock_curses_env: None) -> None:
139+
def test_curses_display_uses_shutil_terminal_size(
140+
mock_curses_env: None,
141+
monkeypatch: pytest.MonkeyPatch,
142+
) -> None:
135143
"""Verify terminal size is queried via shutil.get_terminal_size().
136144
137145
This approach works reliably in tmux/multiplexers because it directly
@@ -141,12 +149,14 @@ def test_curses_display_uses_shutil_terminal_size(mock_curses_env: None) -> None
141149
frame = TextFrame(content_width=10, content_height=2)
142150
frame.set_content(["hello", "world"])
143151

144-
mock_stdscr = MagicMock()
152+
mock_stdscr = unittest.mock.MagicMock()
145153
mock_stdscr.getch.return_value = ord("q")
146154

147-
with patch(
155+
mock_get_size = unittest.mock.MagicMock(return_value=os.terminal_size((120, 40)))
156+
monkeypatch.setattr(
148157
"libtmux.textframe.core.shutil.get_terminal_size",
149-
return_value=os.terminal_size((120, 40)),
150-
) as mock_get_size:
151-
frame._curses_display(mock_stdscr)
152-
mock_get_size.assert_called()
158+
mock_get_size,
159+
)
160+
161+
frame._curses_display(mock_stdscr)
162+
mock_get_size.assert_called()

0 commit comments

Comments
 (0)