Reputation: 2907
Writing test cases to handle successful and failed python subprocess calls, I need to capture subprocess.call
returning code.
Using python unittest.mock module
, is it possible to patch the subprocess.call
function and capture its real system exit code?
Consider an external library with the following code:
## <somemodule.py> file
import subprocess
def run_hook(cmd):
# ...
subprocess.call(cmd, shell=True)
sum_one = 1+1
return sum_one
I can't modify the function run_hook
. It is not part of my code. But, the fact is that subprocess.call
is being called among other statements.
Here we have a snippet of code returning a forced system exit error code 1:
## <somemodule.py> tests file
import subprocess
from somemodule import run_hook
try:
from unittest.mock import patch
except ImportError:
from mock import patch
@patch("subprocess.call", side_effect=subprocess.call)
def test_run_hook_systemexit_not_0(call_mock):
python_exec = sys.executable
cmd_parts = [python_exec, "-c", "'exit(1)'"] # Forced error code 1
cmd = " ".join(cmd_parts)
run_hook(cmd)
call_mock.assert_called_once_with(cmd, shell=True)
# I need to change the following assertion to
# catch real return code "1"
assert "1" == call_mock.return_value(), \
"Forced system exit(1) should return 1. Just for example purpose"
How can I improve this test to capture the expected real value for any subprocess.call
return code?
For compatibility purposes, new subprocess.run
(3.5+) can't be used. This library is still broadly used by python 2.7+ environments.
Upvotes: 0
Views: 3454
Reputation: 2907
A wrapper around subprocess.call
can handle the assertion verification.
In this case, I declare this wrapper as the side_effect
argument in @patch
definition.
In this case, the following implementation worked well.
import sys
import unittest
try:
from unittest.mock import patch
except ImportError:
from mock import patch
def subprocess_call_assert_wrap(expected, message=None):
from subprocess import call as _subcall
def _wrapped(*args, **kwargs):
if message:
assert expected == _subcall(*args, **kwargs), message
else:
assert expected == _subcall(*args, **kwargs)
return _wrapped
class TestCallIsBeingCalled(unittest.TestCase):
@patch("subprocess.call", side_effect=subprocess_call_assert_wrap(expected=0))
def test_run_hook_systemexit_0(self, call_mock):
python_exec = sys.executable
cmd_parts = [python_exec, "-c", "'exit(0)'"]
cmd = " ".join(cmd_parts)
run_hook(cmd)
call_mock.assert_called_once_with(cmd, shell=True)
@patch("subprocess.call", side_effect=subprocess_call_assert_wrap(expected=1))
def test_run_hook_systemexit_not_0(self, call_mock):
python_exec = sys.executable
cmd_parts = [python_exec, "-c", "'exit(1)'"]
cmd = " ".join(cmd_parts)
run_hook(cmd)
call_mock.assert_called_once_with(cmd, shell=True)
After some tests with taking this approach, it seems possible to use for a more general purpose calls, like:
def assert_wrapper(expected, callable, message=None):
def _wrapped(*args, **kwargs):
if message:
assert expected == callable(*args, **kwargs), message
else:
assert expected == callable(*args, **kwargs)
return _wrapped
This is not the best approach, but it seems reasonable.
There is some best known lib with similar behavior that I can use in this project?
Upvotes: 0
Reputation: 22982
About subprocess.call, the documentation says:
Run the command described by args. Wait for command to complete, then return the returncode attribute.
All you need to do is to modify your run_hook
function and return the exit code:
def run_hook(cmd):
# ...
return subprocess.call(cmd, shell=True)
This will simply your test code.
def test_run_hook_systemexit_not_0():
python_exec = sys.executable
args = [python_exec, "-c", "'exit(1)'"]
assert run_hook(args) == 1
My advice: use subprocess.run
instead
Edit
If you want to check the exit code of subprocess.call
you need to patch it with your own version, like this:
import subprocess
_original_call = subprocess.call
def assert_call(*args, **kwargs):
assert _original_call(*args, **kwargs) == 0
Then, you use assert_call
as a side effect function for your patch:
from unittest.mock import patch
@patch('subprocess.call', side_effect=assert_call)
def test(call):
python_exec = sys.executable
args = [python_exec, "-c", "'exit(1)'"]
run_hook(args)
Upvotes: 1