How do I set sys.argv so I can unit test it?
PythonPython Problem Overview
I would like to set
sys.argv
so I can unit test passing in different combinations. The following doesn't work:
#!/usr/bin/env python
import argparse, sys
def test_parse_args():
global sys.argv
sys.argv = ["prog", "-f", "/home/fenton/project/setup.py"]
setup = get_setup_file()
assert setup == "/home/fenton/project/setup.py"
def get_setup_file():
parser = argparse.ArgumentParser()
parser.add_argument('-f')
args = parser.parse_args()
return args.file
if __name__ == '__main__':
test_parse_args()
Then running the file:
pscripts % ./test.py
File "./test.py", line 4
global sys.argv
^
SyntaxError: invalid syntax
pscripts %
Python Solutions
Solution 1 - Python
Changing sys.argv at runtime is a pretty fragile way of testing. You should use mock's patch functionality, which can be used as a context manager to substitute one object (or attribute, method, function, etc.) with another, within a given block of code.
The following example uses patch()
to effectively "replace" sys.argv
with the specified return value (testargs
).
try:
# python 3.4+ should use builtin unittest.mock not mock package
from unittest.mock import patch
except ImportError:
from mock import patch
def test_parse_args():
testargs = ["prog", "-f", "/home/fenton/project/setup.py"]
with patch.object(sys, 'argv', testargs):
setup = get_setup_file()
assert setup == "/home/fenton/project/setup.py"
Solution 2 - Python
test_argparse.py
, the official argparse
unittest file, uses several means of setting/using argv
:
parser.parse_args(args)
where args
is a list of 'words', e.g. ['--foo','test']
or --foo test'.split()
.
old_sys_argv = sys.argv
sys.argv = [old_sys_argv[0]] + args
try:
return parser.parse_args()
finally:
sys.argv = old_sys_argv
This pushes the args onto sys.argv
.
I just came across a case (using mutually_exclusive_groups
) where ['--foo','test']
produces different behavior than '--foo test'.split()
. It's a subtle point involving the id
of strings like test
.
Solution 3 - Python
global
only exposes global variables within your module, and sys.argv
is in sys
, not your module. Rather than using global sys.argv
, use import sys
.
You can avoid having to change sys.argv
at all, though, quite simply: just let get_setup_file
optionally take a list of arguments (defaulting to None
) and pass that to parse_args
. When get_setup_file
is called with no arguments, that argument will be None
, and parse_args
will fall back to sys.argv
. When it is called with a list, it will be used as the program arguments.
Solution 4 - Python
It doesn't work because you're not actually calling get_setup_file
. Your code should read:
import argparse
def test_parse_args():
sys.argv = ["prog", "-f", "/home/fenton/project/setup.py"]
setup = get_setup_file() # << You need the parentheses
assert setup == "/home/fenton/project/setup.py"
Solution 5 - Python
I achieved this by creating an execution manager that would set the args of my choice and remove them upon exit:
import sys
class add_resume_flag(object):
def __enter__(self):
sys.argv.append('--resume')
def __exit__(self, typ, value, traceback):
sys.argv = [arg for arg in sys.argv if arg != '--resume']
class MyTestClass(unittest.TestCase):
def test_something(self):
with add_resume_flag():
...
Solution 6 - Python
I like to use unittest.mock.patch()
. The difference to patch.object()
is that you don't need a direct reference to the object you want to patch but use a string.
from unittest.mock import patch
with patch("sys.argv", ["file.py", "-h"]):
print(sys.argv)
Solution 7 - Python
You'll normally have command arguments. You need to test them. Here is how to unit test them.
-
Assume program may be run like:
% myprogram -f setup.py
-
We create a list to mimic this behaviour. See line (4)
-
Then our method that parses args, takes an array as an argument that is defaulted to
None
. See line (7) -
Then on line (11) we pass this into
parse_args
, which uses the array if it isn'tNone
. If it isNone
then it defaults to usingsys.argv
.1: #!/usr/bin/env python 2: import argparse 3: def test_parse_args(): 4: my_argv = ["-f", "setup.py"] 5: setup = get_setup_file(my_argv) 6: assert setup == "setup.py" 7: def get_setup_file(argv=None): 8: parser = argparse.ArgumentParser() 9: parser.add_argument('-f') 10: # if argv is 'None' then it will default to looking at 'sys.argv'
11: args = parser.parse_args(argv) 12: return args.f 13: if name == 'main': 14: test_parse_args()
Solution 8 - Python
Very good question.
The trick to setting up unit tests is all about making them repeatable. This means that you have to eliminate the variables, so that the tests are repeatable. For example, if you are testing a function that must perform correctly given the current date, then force it to work for specific dates, where the date chosen does not matter, but the chosen dates match in type and range to the real ones.
Here sys.argv will be an list of length at least one. So create a "fakemain" that gets called with a list. Then test for the various likely list lengths, and contents. You can then call your fake main from the real one passing sys.argv, knowing that fakemain works, or alter the "if name..." part to do perform the normal function under non-unit testing conditions.
Solution 9 - Python
You can attach a wrapper around your function, which prepares sys.argv before calling and restores it when leaving:
def run_with_sysargv(func, sys_argv):
""" prepare the call with given sys_argv and cleanup afterwards. """
def patched_func(*args, **kwargs):
old_sys_argv = list(sys.argv)
sys.argv = list(sys_argv)
try:
return func(*args, **kwargs)
except Exception, err:
sys.argv = old_sys_argv
raise err
return patched_func
Then you can simply do
def test_parse_args():
_get_setup_file = run_with_sysargv(get_setup_file,
["prog", "-f", "/home/fenton/project/setup.py"])
setup = _get_setup_file()
assert setup == "/home/fenton/project/setup.py"
Because the errors are passed correctly, it should not interfere with external instances using the testing code, like pytest
.