How to Mock subprocess Calls in Python with unittest.mock

A Python script that wraps Vagrant up/destroy commands using subprocess, with tests that mock the subprocess call to simulate outputs and errors.

Easy Python 3.9+ Aug 9, 2026 Automation & scripting 15 views 0 copies

Python code

38 lines
Python 3.9+
import subprocess
from unittest.mock import patch, Mock


def run_vagrant(action: str) -> str:
    result = subprocess.run(
        ["vagrant", action],
        capture_output=True,
        text=True,
        check=False,
    )
    return result.stdout.strip()


def vagrant_wrapper(action: str) -> str:
    if action not in ("up", "destroy"):
        raise ValueError(f"Unsupported action: {action}")
    output = run_vagrant(action)
    return f"Vagrant {action} done: {output or 'no output'}"


if __name__ == "__main__":
    fake_output = "==> default: VM created"
    with patch("__main__.run_vagrant", return_value=fake_output) as mock_run:
        result = vagrant_wrapper("up")
        mock_run.assert_called_once_with("up")
        print(result)

    with patch("__main__.run_vagrant", return_value="") as mock_run:
        result = vagrant_wrapper("destroy")
        mock_run.assert_called_once_with("destroy")
        print(result)

    with patch("__main__.run_vagrant", side_effect=ValueError("Invalid command")):
        try:
            vagrant_wrapper("invalid")
        except ValueError as error:
            print(f"Error caught: {error}")

Output

stdout
Vagrant up done: ==> default: VM created
Vagrant destroy done: no output
Error caught: Invalid command

How it works

The run_vagrant function uses subprocess.run with capture_output=True and text=True to capture stdout as a string. The wrapper validates the action, calls run_vagrant, and formats the output. patch replaces run_vagrant with a Mock to avoid real subprocess execution, letting you assert calls and control return values. The side_effect parameter lets the mock raise exceptions, simulating failure paths without touching the real system.

Common mistakes

  • Forgetting to patch the correct module path (e.g., `__main__` vs the wrapper's module) results in real subprocess calls.
  • Using `check=True` in `subprocess.run` raises an exception on non-zero exit, which you may need to handle with `check=False`.
  • Not using `text=True` or `capture_output=True` leaves stdout as bytes, complicating string operations.

Variations

  1. Use `subprocess.check_output` with `stderr` handling for simpler capture.
  2. Use a context manager `with patch.object(module, 'run_vagrant')` when the function is imported.

Real-world use cases

  • Automating infrastructure provisioning in CI pipelines where Vagrant commands run and output is parsed for status.
  • Writing unit tests for deployment scripts that invoke VirtualBox or other CLI tools, ensuring deterministic behavior.
  • Building a wrapper CLI that manages local development environments and logs Vagrant output for debugging.

Sponsored

Run this sample

Open the browser IDE to tweak the example and see results without installing anything.

Open editor

More from Automation & scripting

Related tutorials and quizzes for this topic.