Mocking a subprocess call in Python

If you want to check that the mocked object was called with a certain parameter, you can add the side_effect argument to the mock.patch decorator.

The return value of the side_effect function determines the return value of subprocess.Popen. If the side_effect_func returns DEFAULT, subprocess.Popen will be called in a normal way.

from unittest import mock, TestCase
from unittest.mock import DEFAULT
import subprocess


def run_script(script_path, my_arg):
    process = subprocess.Popen([script_path, my_arg])
    return process


def side_effect_func(*args, **kwargs):

    # Print the arguments
    print(args)
    
    # If 'bar' is contained within the arguments, return 'foo'
    if any(['bar' in arg for arg in args]):
        return 'foo'
    
    # If 'bar' is not contained within the arguments, run subprocess.Popen
    else:
        return DEFAULT


class TestRunScriptClass(TestCase):

    @mock.patch("subprocess.Popen", side_effect=side_effect_func)
    def test_run_script(self, mock):
        # Run the function
        process = run_script(script_path='my_script.py', my_arg='bar')
        
        # Assert if the mock object was called
        self.assertTrue(mock.called)
        
        # Assert if the mock object returned 'foo' when providing 'bar'
        self.assertEqual(process, 'foo')


It seems unusual to me that you use the patch decorator over the run_script function, since you don't pass a mock argument there.

How about this:

from unittest import mock
import subprocess

def run_script(file_path):
    process = subprocess.Popen(["myscript", -M, file_path], stdout=subprocess.PIPE)
    output, err = process.communicate()
    return process.returncode


@mock.patch("subprocess.Popen")
def test_run_script(self, mock_subproc_popen):
    process_mock = mock.Mock()
    attrs = {"communicate.return_value": ("output", "error")}
    process_mock.configure_mock(**attrs)
    mock_subproc_popen.return_value = process_mock
    am.account_manager("path")  # this calls run_script somewhere, is that right?
    self.assertTrue(mock_subproc_popen.called)

Right now, your mocked subprocess.Popen seems to return a tuple, causeing process.communicate() to raise TypeError: 'tuple' object is not callable.. Therefore it's most important to get the return_value on mock_subproc_popen just right.


The testfixtures library (docs, github) can mock the subprocess package.

Here's an example on using the mock subprocess.Popen:

from unittest import TestCase

from testfixtures.mock import call
from testfixtures import Replacer, ShouldRaise, compare
from testfixtures.popen import MockPopen, PopenBehaviour


class TestMyFunc(TestCase):
    def setUp(self):
        self.Popen = MockPopen()
        self.r = Replacer()
        self.r.replace(dotted_path, self.Popen)
        self.addCleanup(self.r.restore)

    def test_example(self):
        # set up
        self.Popen.set_command("svn ls -R foo", stdout=b"o", stderr=b"e")

        # testing of results
        compare(my_func(), b"o")

        # testing calls were in the right order and with the correct parameters:
        process = call.Popen(["svn", "ls", "-R", "foo"], stderr=PIPE, stdout=PIPE)
        compare(Popen.all_calls, expected=[process, process.communicate()])

    def test_example_bad_returncode(self):
        # set up
        Popen.set_command("svn ls -R foo", stdout=b"o", stderr=b"e", returncode=1)

        # testing of error
        with ShouldRaise(RuntimeError("something bad happened")):
            my_func()

Tags:

Python

Mocking