2
votes

I have a function inside a module that creates an argparse:

def get_options(prog_version='1.0', prog_usage='', misc_opts=None):
     options = [] if misc_opts is None else misc_opts
     parser = ArgumentParser(usage=prog_usage) if prog_usage else ArgumentParser()
     parser.add_argument('-v', '--version', action='version', version='%(prog)s {}'.format(prog_version))
     parser.add_argument('-c', '--config', dest='config', required=True, help='the path to the configuration file')

    for option in options:
        if 'option' in option and 'destination' in option:
            parser.add_argument(option['option'],
                                dest=option.get('destination', ''),
                                default=option.get('default', ''),
                                help=option.get('description', ''),
                                action=option.get('action', 'store'))

    return parser.parse_args()

A sample myapp.py would be:

my_options = [
    {
        "option": "-s",
        "destination": "remote_host",
        "default": "127.0.0.1",
        "description": "The remote server name or IP address",
        "action": "store"
    },
]

# Get Command Line Options
options = get_options(misc_opts=my_options)
print options.config
print options.remote_host

and this will be called as:

$> python myapp.py -c config.yaml
$> config.yaml
   127.0.0.1

Now, I am trying to create a unit test for this function but my problem is that I can't pass command line parameters via test code.

# mytest.py
import unittest
from mymodule import get_options

class argParseTestCase(unittest.TestCase):
     def test_parser(self):
         options = get_options()
         # ...pass the command line arguments...
         self.assertEquals('config.yaml', options.config) # ofcourse this fails because I don't know how I will pass the command line arguments

My problem is that I need to pass the command line arguments to get_options() but I don't know how to do it properly.

Expected proper call: python mytest.py (-c config.yaml should be passed inside the test code somehow.)

What is "working"/not working right now:

  1. python mytest.py -c config.yaml is also not working. Returns AttributeError: 'module' object has no attribute 'config' since it expects me to call argParseTestCase instead. In other words, python mytest.py -c argParseTestCase "works" but would ofcourse be an return AssertionError: 'config.yaml' != 'argParseTestCase'
  2. python mytest.py -v to run the unit test in verbose mode also fails. It returns:

    test_parser (main.argParseTestCase) ... mytest.py 1.0 ERROR ERROR: test_parser (main.argParseTestCase)
    Traceback (most recent call last): File "tests/unit_tests/mytest.py", line 376, in test_parser options = get_options() File "/root/test/lib/python2.7/site-packages/mymodule.py", line 61, in get_options return parser.parse_args()
    File "/usr/local/lib/python2.7/argparse.py", line 1701, in parse_args args, argv = self.parse_known_args(args, namespace)
    File "/usr/local/lib/python2.7/argparse.py", line 1733, in parse_known_args namespace, args = self._parse_known_args(args, namespace)
    File "/usr/local/lib/python2.7/argparse.py", line 1939, in _parse_known_args start_index = consume_optional(start_index)
    File "/usr/local/lib/python2.7/argparse.py", line 1879, in consume_optional take_action(action, args, option_string)
    File "/usr/local/lib/python2.7/argparse.py", line 1807, in take_action action(self, namespace, argument_values, option_string)
    File "/usr/local/lib/python2.7/argparse.py", line 1022, in call parser.exit(message=formatter.format_help())
    File "/usr/local/lib/python2.7/argparse.py", line 2362, in exit _sys.exit(status) SystemExit: 0

2

2 Answers

5
votes

I prefer explicitly passing arguments instead of relying on globally available attributes such as sys.argv (which parser.parse_args() does internally). Thus I usually use argparse by passing the list of arguments myself (to main() and subsequently get_options() and wherever you need them):

def get_options(args, prog_version='1.0', prog_usage='', misc_opts=None):
    # ...
    return parser.parse_args(args)

and then pass in the arguments

def main(args):
    get_options(args)

if __name__ == "__main__":
    main(sys.argv[1:])

that way I can replace and test any list of arguments I like

options = get_options(['-c','config.yaml'])
self.assertEquals('config.yaml', options.config) 
4
votes

Your error message stack is hard to read because it is in quoted form rather than code. But I think the -v argument is producing a sys.exit. version is like help - it's supposed to display a message and then exit. The -v is used by unittest, but is also read by your parser.

There is an argparse unittest module, test/test_argparse.py. You may need a development Python installation to see that. Some tests are straightforward, others use specialized testing structure. Some of that special code creates arguments in the same way you do with options.

The are two special issues:

  • generating the input. parse_args uses sys.argv[1:] unless its argv parameter is not None. So you can test a parser by either modifying the sys.argv list (unittest has already used your commandline values), or by passing a argv=None keyword argument into your function and on to parse_args. Trying to make a commandline meant for the unittest code to work with get_options is too complicated.

  • trapping the output, especially the sys.exit generated by errors. One option is to subclass ArgumentParser and give it a different error and/or exit method. Another is to wrap the function call in a try block.

unittest takes -c argument, but with a different syntax and meaning

 -c, --catch      Catch control-C and display results

and -v is verbose, not version.

=============

This tests the config argument (in a self contained one file form)

import unittest
import sys
#from mymodule import get_options

def get_options(argv=None, prog_version='1.0', prog_usage='', misc_opts=None):
    # argv is optional test list; uses sys.argv[1:] is not provided
    from argparse import ArgumentParser
    options = [] if misc_opts is None else misc_opts
    parser = ArgumentParser(usage=prog_usage) if prog_usage else ArgumentParser()
    parser.add_argument('-v', '--version', action='version', version='%(prog)s {}'.format(prog_version))
    parser.add_argument('-c', '--config', dest='config', help='the path to the configuration file')

    for option in options:
        if 'option' in option and 'destination' in option:
            parser.add_argument(option['option'],
                                dest=option.get('destination', ''),
                                default=option.get('default', ''),
                                help=option.get('description', ''),
                                action=option.get('action', 'store'))

    args = parser.parse_args(argv)
    print('args',args)
    return args

class argParseTestCase(unittest.TestCase):
     def test_config(self):
         sys.argv[1:]=['-c','config.yaml']       
         options = get_options()
         self.assertEquals('config.yaml', options.config) 
     def test_version(self):
         sys.argv[1:]=['-v']   
         with self.assertRaises(SystemExit):
                    get_options() 
         # testing version message requires redirecting stdout
     # similarly for a misc_opts test

if __name__=='__main__':
    unittest.main()