57

I have a module that imports unittest and has some TestCases. I would like to accept some command-line options (for example below, the name of a data file), but when I try to pass the option I get the message option -i not recognized. Is it possible to have unittest + provide options to the app (note: I'm using optparse to handle the options)? Thanks.

$ python test_app_data.py -i data_1.txt

option -i not recognized

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

follow-up: this is an implementation of the suggested solution:

import cfg_master  #has the optparse option-handling code

...

if __name__ == '__main__':    
    #add you app's options here...
    options_tpl = ('-i', '--in_dir', '-o', '--out_dir')
    del_lst = []
    for i,option in enumerate(sys.argv):
        if option in options_tpl:
            del_lst.append(i)
            del_lst.append(i+1)

    del_lst.reverse()
    for i in del_lst:
        del sys.argv[i]
        
    unittest.main()
Hirusha Fernando
  • 1,156
  • 10
  • 29
jd.
  • 4,543
  • 7
  • 34
  • 40

6 Answers6

72

Building on Alex's answer, it's actually pretty easy to do using argparse:

if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument('--input', default='My Input')
    parser.add_argument('filename', default='some_file.txt')
    parser.add_argument('unittest_args', nargs='*')

    args = parser.parse_args()
    # TODO: Go do something with args.input and args.filename

    # Now set the sys.argv to the unittest_args (leaving sys.argv[0] alone)
    sys.argv[1:] = args.unittest_args
    unittest.main()

I haven't tested all of the flags you can pass into unittest to see if they work or not, but passing test names in does work, e.g.:

python test.py --input=foo data.txt MyTest

Runs MyTest with foo and data.txt.

tghw
  • 25,208
  • 13
  • 70
  • 96
  • 12
    Nice! Even better, `unittest.main()` accepts an argv parameter so you don't have to mess with the global sys.argv, e.g. like so: `unit_argv = [sys.argv[0]] + args.unittest_args; unittest.main(argv=unit_argv)` – wutz Jan 24 '13 at 10:46
  • 8
    You can use `.parse_known_args()` instead and not need to use a `nargs='*'` option; see [Python: run a unittest.TestCase without calling unittest.main()?](http://stackoverflow.com/a/17259773) for my version. – Martijn Pieters Aug 14 '13 at 20:33
  • I linked this answer from the question referenced by @MartijnPieters, because the approach appealed to me. However, when I implemented this answer did not work for me when passing flags (--failfast and --catch), so I reverted to Martijn's answer. – Chris Morlier Aug 15 '13 at 23:43
  • now I wish there was an easy answer like this for `pytest`! – nmz787 May 11 '19 at 02:45
  • I suggest `nargs=argparse.REMAINDER` instead of `nargs='*'`. The latter will choke on any `--flags` (such as `--verbose`). – cheshirekow Jul 19 '19 at 20:37
33

In your if __name__ == '__main__': section, which you're not showing us, you'll need to optparse and then del sys.argv[1:] before you pass control to unittest code, so that the latter code doesn't try to interpret your command line options again when you've already dealt with them. (It's a bit harder to have some options of your own and also pass some down to unittest, though it can be done if you do have such complex needs).

Tats_innit
  • 33,991
  • 10
  • 71
  • 77
Alex Martelli
  • 854,459
  • 170
  • 1,222
  • 1,395
2

I thought I'd share my solution for adding a --debug switch to the test to control the logger:

if __name__=='__main__':
     parser = argparse.ArgumentParser(description="Build a compilation script")
     parser.add_argument('--debug', help='Turn on debug messages', action='store_true', default=False)

     args = parser.parse_args()

     if args.debug:
         log_level = logging.DEBUG
     else:
         log_level = logging.INFO

     logging.basicConfig(level=log_level)
     sys.argv.pop()
     unittest.main()

Then I extended unittest.TestCase to add logging:

 class mcs_TestCase(unittest.TestCase, object):
     def __init__(self, *args, **kwargs):
         super(mcs_TestCase,self).__init__(*args,**kwargs)
         logging.basicConfig()
         self.logger = logging.getLogger(__name__)
         ...

Now I can turn messages on and off in my test using --debug, but it gets ignored in regular regressions.

Ray Salemi
  • 5,247
  • 4
  • 30
  • 63
1

For small standalone apps, I use an initial sentinel option (-t) and call unittest.main() before calling argparse.ArgumentParser()

if __name__ == "__main__":
    if len(sys.argv) > 1 and sys.argv[1] in ["-t", "--test"]:
        del(sys.argv[1])
        sys.exit(unittest.main()) # pass sys.argv[

    p = argparse.ArgumentParser()
    . . .
qneill
  • 1,643
  • 14
  • 18
1

In addition to the other answers I'd like to provide a different view on the question. I know the question asked for a way to provide command line options to unit tests, however the actual problem that is stated is just "how can I pass parameters to unit tests".

This can also be achieved with environment variables.

Here's an example how you could provide different data to a test locally and on a build server:

my-script.py:

from os import environ

database = MyDatabase(
    user = 'root',
    password = 'some-pw',
    host = environ['MYSQL_HOST'],
    port = environ['MYSQL_TCP_PORT']
)

test-local.sh:

MYSQL_TCP_PORT="3311"
MYSQL_HOST="localhost"
python3.7 -m unittest

test-ci.sh:

MYSQL_TCP_PORT="3306"
MYSQL_HOST="my-database"
python3.7 -m unittest
bersling
  • 17,851
  • 9
  • 60
  • 74
-8

You should not take arguments and options to run unittests, as you make them run under different, less predictable conditions this way. You should figure out why you need to run tests with different data, and make you test suite complete enough to cover the ground of all data sets without being run differently each time.

ironfroggy
  • 7,991
  • 7
  • 33
  • 44
  • 1
    i receive weekly data reports (files); i'd like to feed these new reports to the unittests, before generating public reports. If for some reason the structure or type of the data has changed (e.g. a new column field was added, a data range changed and a bug is unveiled) I'd like to catch them with the tests. Hope this makes sense. – jd. Jun 23 '09 at 00:13
  • 2
    How about for conditional test skipping? I run all my test cases by doing unittest's test discovery, but it'd be nice to be able to have annotations on certain tests like @skipUnless('-full' in sys.argv) so that I can have an easy command line way of running all tests, or only some. – Adam Parkin Nov 09 '11 at 22:54
  • 1
    And if you're wondering why I may want to do that, how about for easy separation of quick unit tests vs longer regression or stress tests? – Adam Parkin Nov 09 '11 at 23:08
  • 2
    As a QA test engineer, I would agree with ironfroggy's unfairly moderated down answer. When you have arrived at a controversial solution to a problem - such as variable and argparse based tests - it's a good idea to review the problem again to see if there are better solutions. For example if you want to take "one test" and run it against a huge matrix of test configuration, there are ways to do that. These days (2015) there is also "Py.test" which is a departure from unittest and allows easy generation of testcase ids from data, against a test function. It's less boilerplate than unittest. – Scott Prive May 21 '15 at 14:22
  • IOW - don't ever change tests - make new ones. (Rule of thumb, not absolute, but mostly never do this) – Scott Prive May 21 '15 at 14:25
  • 3
    I understand the sentiment and warning about the OPs idea leading to potential problems. But this belongs as a *comment*, not as an answer. The OP is asking for a solution to his requirements, not a critique on the advisability of his requirements. – JL Peyret Jul 24 '15 at 19:31
  • This depends a lot on different factors like, for example, specific testing environments that you would like to test only on certain occasions. In my case, I want to test that my source code is compatible with the testing environment always and, sometimes, I want to test the same source code against the production environment. – nsx Nov 26 '20 at 04:23