Newsletter
TechAnV Blog
Get updates on security engineering, Rust, eBPF, and DevSecOps. No spam, unsubscribe anytime.
Check your inbox and click the confirmation link to complete your subscription.
Mocking subprocess with pytest-subprocess#
For apple-notes-to-sqlite I needed to write some tests that simulated executing the osascript command using the Python subprocess module.
I wanted my tests to run on Linux CI machines, where that command would not exist.
After failing to use unittest.mock.patch to solve this, I went looking for alternatives. I found pytest-subprocess.
Here’s the relevant section of the test I wrote:
1from apple_notes_to_sqlite.cli import cli, COUNT_SCRIPT2
3FAKE_OUTPUT = b"""4The stuff I would expect to be returned by log lines in5my osascript script.6"""7
8def test_apple_notes_to_sqlite_dump(fp):9 fp.register_subprocess(["osascript", "-e", COUNT_SCRIPT], stdout=b"2")10 fp.register_subprocess(["osascript", "-e", fp.any()], stdout=FAKE_OUTPUT)11 runner = CliRunner()12 with runner.isolated_filesystem():13 result = runner.invoke(cli, ["--dump"])14 # ...fp is the fixture provided by the package (you need to pip install pytest-subprocess for this to work).
COUNT_SCRIPT here is the first of my osascript constants. It looks like this (in cli.py):
1COUNT_SCRIPT = """2tell application "Notes"3 set noteCount to count of notes4end tell5log noteCount6"""That first fixture line says that any time my program calls osascript -e that-count-script the return value sent to standard output should be a binary string 2.
1fp.register_subprocess(["osascript", "-e", COUNT_SCRIPT], stdout=b"2")The second call to subprocess made by my script is more complicated - it involves a script that is dynamically generated.
1fp.register_subprocess(["osascript", "-e", fp.any()], stdout=FAKE_OUTPUT)I eventually figured that using fp.any() was easier than specifying the exact script. This is a wildcard value which matches any string. It returns the full FAKE_OUTPUT variable as the simulated standard out.
What’s useful about pytest-subprocess is that it works for both subprocess.check_output() and more complex subprocess.Popen() calls - both of which I was using in this script.