Andre Pastore
Andre Pastore

Reputation: 2907

Mocking python subprocess.call function and capture its system exit code

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

Answers (2)

Andre Pastore
Andre Pastore

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

Laurent LAPORTE
Laurent LAPORTE

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

Related Questions