Reputation: 807
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 python script.py
Here is a example oldscript.py
:
import socket
def testfunction():
return socket.gethostname()
def unmockable():
return "somedata"
if __name__ == '__main__':
result = testfunction()
result = unmockable()
print(result)
I'm using 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 subprocess
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?
Upvotes: 1
Views: 1344
Reputation: 46
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:
lib.py
:
def unmockable():
return "somedata"
oldscript.py
:
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 pip
).
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.
Upvotes: 3