Mocking system commands

Mocking is a technique to replace parts of a system with interfaces that don’t do anything, but which your tests can check whether and how they were called. The unittest.mock module in Python 3 lets you mock Python functions and classes. The tools described here let you mock external commands.

Commands are mocked by creating a real file in a temporary directory which is added to the PATH environment variable, not by replacing Python functions. So if you mock git, and your Python code runs a shell script which calls git, it will be the mocked command that it runs.

By default, mocked commands record each call made to them, so that your test can check these. Using the MockCommand API, you can change what a mocked command does.

Note

Mocking a command affects all running threads or coroutines in a program. There’s no way to mock a command for only the current thread/coroutine, because it uses environment variables, which are global.

testpath.assert_calls(cmd, args=None)

Assert that a block of code runs the given command.

If args is passed, also check that it was called at least once with the given arguments (not including the command name).

Use as a context manager, e.g.:

with assert_calls('git'):
    some_function_wrapping_git()
    
with assert_calls('git', ['add', myfile]):
    some_other_function()
class testpath.MockCommand(name, content=None, python='')

Context manager to mock a system command.

The mock command will be written to a directory at the front of $PATH, taking precedence over any existing command with the same name.

The python parameter accepts a string of code for the command to run, in addition to the default behaviour of recording calls to the command. This will run with the same Python interpreter as the calling code, but in a new process.

The content parameter gives extra control, by providing a script which will run with no additions. On Unix, it should start with a shebang (e.g. #!/usr/bin/env python) specifying the interpreter. On Windows, it will always be run by the same Python interpreter as the calling code. Calls to the command will not be recorded when content is specified.

classmethod fixed_output(name, stdout='', stderr='', exit_status=0)

Make a mock command, producing fixed output when it is run:

t = 'Sat 24 Apr 17:11:58 BST 2021\n'
with MockCommand.fixed_output('date', t) as mock_date:
    ...

The stdout & stderr strings will be written to the respective streams, and the process will exit with the specified numeric status (the default of 0 indicates success).

This works with the recording mechanism, so you can check what arguments this command was called with.

get_calls()

Get a list of calls made to this mocked command.

For each time the command was run, the list will contain a dictionary with keys argv, env and cwd.

This won’t work if you used the content parameter to alter what the mocked command does.

assert_called(args=None)

Assert that the mock command has been called at least once.

If args is passed, also check that it was called at least once with the given arguments (not including the command name), e.g.:

with MockCommand('rsync') as mock_rsync:
    function_to_test()

mock_rsync.assert_called(['/var/log', 'backup-server:logs'])

This won’t work if you used the content parameter to alter what the mocked command does.