4
  • I am writing unit tests for a Python library using pytest
  • I need to specify a directory for test files to avoid automatic test file discovery, because there is a large sub-directory structure, including many files in the library containing "_test" or "test_" in the name but are not intended for pytest
  • Some files in the library use argparse for specifying command-line options
  • The problem is that specifying the directory for pytest as a command-line argument seems to interfere with using command line options for argparse

To give an example, I have a file in the root directory called script_with_args.py as follows:

import argparse

def parse_args():
    parser = argparse.ArgumentParser(description="description")

    parser.add_argument("--a", type=int, default=3)
    parser.add_argument("--b", type=int, default=5)

    return parser.parse_args()

I also have a folder called tests in the root directory, containing a test-file called test_file.py:

import script_with_args

def test_script_func():
    args = script_with_args.parse_args()
    assert args.a == 3

If I call python -m pytest from the command line, the test passes fine. If I specify the test directory from the command line with python -m pytest tests, the following error is returned:

============================= test session starts =============================
platform win32 -- Python 3.6.5, pytest-3.5.1, py-1.5.3, pluggy-0.6.0
rootdir: C:\Users\Jake\CBAS\pytest-tests, inifile:
plugins: remotedata-0.2.1, openfiles-0.3.0, doctestplus-0.1.3, arraydiff-0.2
collected 1 item

tests\test_file.py F                                                     [100%]

================================== FAILURES ===================================
______________________________ test_script_func _______________________________

    def test_script_func():
        # a = 1
        # b = 2
>       args = script_with_args.parse_args()

tests\test_file.py:13:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
script_with_args.py:9: in parse_args
    return parser.parse_args()
..\..\Anaconda3\lib\argparse.py:1733: in parse_args
    self.error(msg % ' '.join(argv))
..\..\Anaconda3\lib\argparse.py:2389: in error
    self.exit(2, _('%(prog)s: error: %(message)s\n') % args)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = ArgumentParser(prog='pytest.py', usage=None, description='description', f
ormatter_class=<class 'argparse.HelpFormatter'>, conflict_handler='error', add_h
elp=True)
status = 2, message = 'pytest.py: error: unrecognized arguments: tests\n'

    def exit(self, status=0, message=None):
        if message:
            self._print_message(message, _sys.stderr)
>       _sys.exit(status)
E       SystemExit: 2

..\..\Anaconda3\lib\argparse.py:2376: SystemExit
---------------------------- Captured stderr call -----------------------------
usage: pytest.py [-h] [--a A] [--b B]
pytest.py: error: unrecognized arguments: tests
========================== 1 failed in 0.19 seconds ===========================

My question is, how do I specify the test file directory for pytest, without interfering with the command line options for argparse?

Jake Levi
  • 1,329
  • 11
  • 16
  • Your issue is not how to avoid passing CLI args in test session, but rather how to write proper tests for CLI args parsing logic. You have two possibilities for that: mocking the `sys.argv` list or the `ArgumentParser.parse_args` return value, or refactor your `parse_args` function so it accepts the args list. Both ways have examples in the answers to the linked question. – hoefling Aug 06 '18 at 15:12
  • 1
    Possible duplicate of [How do you write tests for the argparse portion of a python module?](https://stackoverflow.com/questions/18160078/how-do-you-write-tests-for-the-argparse-portion-of-a-python-module) – hoefling Aug 06 '18 at 15:12
  • 1
    @hoefling or maybe https://stackoverflow.com/questions/18668947/how-do-i-set-sys-argv-so-i-can-unit-test-it – leafmeal Aug 06 '18 at 16:35

3 Answers3

2

parse_args() without argument reads the sys.argv[1:] list. That will include the 'tests' string.

pytests also uses that sys.argv[1:] with its own parser.

One way to make your parser testable is provide an optional argv:

def parse_args(argv=None):
    parser = argparse.ArgumentParser(description="description")

    parser.add_argument("--a", type=int, default=3)
    parser.add_argument("--b", type=int, default=5)

    return parser.parse_args(argv)

Then you can test it with:

parse_args(['-a', '4'])

and use it in for real with

parse_args()

Changing the sys.argv is also good way. But if you are going to the work of putting the parser in a function like this, you might as well give it this added flexibility.

hpaulj
  • 221,503
  • 14
  • 230
  • 353
  • This does not work for me when trying to inject cmd-line arguments for pytest tests. Unittest.mock in itself works but has no effect in changing the command-line argument to the value I inject programatically. – Dr. Hillier Dániel Feb 04 '19 at 14:49
1

To add to hpaulj's answer, you can also use a library like unittest.mock to temporarily mask the value of sys.argv. That way your parse args command will run using the "mocked" argv but the actual sys.argv remains unchanged.

When your tests call parse_args() they could do it like this:

with unittest.mock.patch('sys.argv', ['--a', '1', '--b', 2]):
    parse_args()
leafmeal
  • 1,824
  • 15
  • 15
1

I ran into a similar problem with test discovery in VS Code. The run adapter in VS Code passes in parameters that my program does not understand. My solution was to make the parser accepts unknown arguments.

Change:

return parser.parse_args()

To:

args, _ = parser.parse_known_args()
return args
Code Different
  • 90,614
  • 16
  • 144
  • 163