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()