How to run pytest with a specified test directory

2019-08-23 00:29发布

问题:

  • 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?

回答1:

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.



回答2:

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()