What is the best way to mock os.system for unit test (PyTest)

Viewed 9255

I have a Python script that does multiple os.system calls. Asserting against the series of them as a list of strings will be easy (and relatively elegant).

What isn't so easy is intercepting (and blocking) the actual calls. In the script in question, I could abstract os.system in the SUT (*) like so:

os_system = None

def main():
    return do_the_thing(os.system)

def do_the_thing(os_sys):
    global os_system
    os_system = os_sys

    # all other function should use os_system instead of os.system

My test invokes my_script.do_the_thing() instead of my_script.main() of course (leaving a tiny amount of untested code).

Alternate option: I could leave the SUT untouched and replace os.system globally in the test method before invoking main() in the SUT.

That leaves me with new problems in that that's a global and lasting change. Fine, so I'd use a try/finally in the same test method, and replace the original before leaving the test method. That'd work whether the test method passes or fails.

Is there a safe and elegant setup/teardown centric way of doing this for PyTest, though?

Additional complications: I want to do the same for stdout and stderr. Yes, it really is a main() script that I am testing.

  • SUT == System Under Test
2 Answers

Just want to add an important detail.

If your code uses import system for example like this:

myls.py:

import os


def do_ls():
    os.system('ls')

Then the patch in your test should look like this:

test_myls.py:

from unittest.mock import patch


@patch('os.system')
def test_do_ls(mock_system):
    do_ls()
    mock_system.assert_called()

However, if the code uses from os import system, e.g. like this:

myls.py:

from os import system


def do_ls():
    system('ls')

Then the patch in your test should look like this:

test_myls.py:

from unittest.mock import patch


@patch('myls.system')
def test_do_ls(mock_system):
    do_ls()
    mock_system.assert_called()

This eluded me for a bit because I had forgotten to read the section on where to patch as I originally intended. If patching does not seem to work, this is one of the points to look at.

Related