test: add shared fake_process fixture for complete Popen mocking

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
James Smith
2026-06-11 16:44:04 +01:00
parent d4652017f5
commit c177dd354a
2 changed files with 49 additions and 0 deletions
+25
View File
@@ -138,3 +138,28 @@ def test_db(tmp_path):
conn.execute("PRAGMA journal_mode = WAL")
yield conn
conn.close()
@pytest.fixture
def fake_process():
"""Factory for complete subprocess.Popen replacements.
Hand-rolled Popen mocks keep missing two things: __enter__ (subprocess.run
wraps Popen in a context manager) and a communicate() tuple. Use this
factory instead of building MagicMock processes inline.
"""
def _make(returncode=0, stdout="", stderr="", running=True, pid=12345):
proc = MagicMock()
proc.poll.return_value = None if running else returncode
proc.returncode = returncode
proc.pid = pid
proc.wait.return_value = returncode
proc.communicate.return_value = (stdout, stderr)
proc.stdout.read.return_value = stdout
proc.stderr.read.return_value = stderr
proc.stdin = MagicMock()
proc.__enter__.return_value = proc
return proc
return _make
+24
View File
@@ -0,0 +1,24 @@
"""Tests for shared conftest fixtures."""
import subprocess
from unittest.mock import patch
class TestFakeProcess:
def test_works_with_subprocess_run(self, fake_process):
"""subprocess.run() must unpack communicate() and enter the context manager."""
proc = fake_process(stdout="hello", stderr="", returncode=0)
with patch("subprocess.Popen", return_value=proc):
result = subprocess.run(["anything"], capture_output=True, text=True, timeout=5)
assert result.stdout == "hello"
def test_running_process_defaults(self, fake_process):
proc = fake_process()
assert proc.poll() is None # still running
assert proc.pid == 12345
assert proc.communicate() == ("", "")
def test_exited_process(self, fake_process):
proc = fake_process(running=False, returncode=1, stderr=b"device busy")
assert proc.poll() == 1
assert proc.stderr.read() == b"device busy"