I need to test some legacy code, among which there are a number of Python scripts.
By script I mean Python code not within a class or module, just within a unique file and executed with
Here is a example
import socket def testfunction(): return socket.gethostname() def unmockable(): return "somedata" if __name__ == '__main__': result = testfunction() result = unmockable() print(result)
pytest-console-scripts to test this as it’s “inprocess” launcher makes it possible to actually mock some things.
AFAIU, there’s no way to mock any call made within a Python script when it is ran with
pytest-console-scripts makes this possible, and indeed mocks to external functions work.
Here’s a test case for the above :
import socket from pytest_console_scripts import ScriptRunner from pytest_mock import MockerFixture class TestOldScript: def test_success(self, script_runner: ScriptRunner, mocker: MockerFixture) -> None: mocker.patch('socket.gethostname', return_value="myhostname") mocker.patch('oldscript.unmockable', return_value="mocked!", autospec=True) ret = script_runner.run('oldscript.py', print_result=True, shell=True) socket.gethostname.assert_called_with() assert ret.success assert ret.stdout == 'mocked!' assert ret.stderr is None
This is failing as
unmockable cannot be mocked this way.
The call to
socket.gethostname() can be successfully mocked, but can the
unmockable function be mocked? That is my issue.
Would there be another strategy to test such Python scripts and be able to mock internal functions?
The problem here is that when the script is executed,
oldscript.py is not being imported into
oldscript namespace, it’s instead in
__main__ (that’s why the condition of the
if at the bottom of the script is true). Your code successfully patchess
oldscript.unmockable, but the script is calling
__main__.unmockable and that one is indeed unmockable.
I see two ways to get around this:
You can split the code that you would like to mock into another module that’s imported by the main script. For example if you split
oldscript.py into two files like this:
def unmockable(): return "somedata"
import socket import lib def testfunction(): return socket.gethostname() if __name__ == '__main__': result = testfunction() print('testfunction:', result) result = lib.unmockable() print('unmockable:', result)
then you can mock
lib.unmockable in the test and everything works as expected.
Another approach is to use a
console_scripts entry point in
setup.py (see here for more info on this). This is a more sophisticated approach that would be a good fit for python packages that have
setup.py and are installed (e.g. via
When you set up your script to be installed and called this way, it becomes available in the
PATH as e.g.
oldscript and then you can call it from tests with:
script_runner.run('oldscript') # Without the .py
These installed console scripts are imported and executed using a wrapper that
setup.py creates, so
oldscript.py would be imported as
oldscript and again mocking will work.