python-3.xmockingsubprocesspytestpytest-mock

How to mock subprocess.run in pytest?


I have the class InternalProc, defined as following:

class InternalProc:

    @staticmethod
    def get_data():
        try:
            result = subprocess.run(['bridge-client', '--business-credentials'],
                                    stdout=subprocess.PIPE)
            data = json.loads(result.stdout.decode('utf-8'))
            return data
        except Exception:
            logger.error("Unable to fetch the data")
            raise

I want to unit test the get_data(), how should I mock subprocess.run? I also want to assert on whether the exception was raised or not.


Solution

  • Seems like you need two tests to test this method, one that returns data and one that raises an Exception.

    For the one where we return data we simply need to mock subprocess.run. Then we can create another Mock object to mock stdout.decode to return data that json.loads can parse. This means creating a Mock object for the behavior of stdout and then configuring our mocked subprocess.run to use this object.

    For the other test we simply need to use the side_effect kwarg of the Mock object to raise an exception when it is called.

    Given the following folder structure:

    stackoverflow/
    ├── mypackage
    │   ├── __init__.py
    │   └── proc.py
    └── tests
        ├── __init__.py
        └── test_proc.py
    

    The tests we write are shown below.

    from unittest.mock import MagicMock, patch
    
    import pytest
    
    from mypackage.proc import InternalProc
    
    
    @patch("mypackage.proc.subprocess.run")
    def test_get_data_valid(mock_run):
        mock_stdout = MagicMock()
        mock_stdout.configure_mock(
            **{
                "stdout.decode.return_value": '{"A": 3}'
            }
        )
    
        mock_run.return_value = mock_stdout
    
        result = InternalProc.get_data()
        assert result == {"A": 3}
    
    
    @patch("mypackage.proc.subprocess.run", side_effect=Exception("foobar"))
    def test_get_data_invalid(mock_run):
        with pytest.raises(Exception) as exc:
            InternalProc.get_data()
            assert "foobar" in str(exc.value)
    

    Output:

    ======================================= test session starts ========================================
    platform darwin -- Python 3.9.1, pytest-6.2.2, py-1.10.0, pluggy-0.13.1
    rootdir: ***/stackoverflow
    collected 2 items                                                                                  
    
    tests/test_proc.py ..                                                                        [100%]
    
    ======================================== 2 passed in 0.07s =========================================
    

    My suggestion to you, since I have seen you post multiple pytest/mock questions in the last few days is to spend some time reading the documentation for both. Make some toy examples and play around with the two packages. That is the only way you will learn how to mock and where to mock.