Dead Simple: PyTest and ArgParse

Adam Hughes
Programmer’s Journey
2 min readAug 4, 2023

--

You’re writing a Python script with an argparser and want to test it without mocks and hacks. You’ve been googling for 20 minutes. You’ve landed here. Go no further.

Here is a template for a parser that takes a single argument myfile

# coolapp.py
import argparse as ap
import sys

def _parse(args=None) -> ap.Namespace:
parser = ap.ArgumentParser()
parser.add_argument("myfile")
parsed = parser.parse_args(args)
start(parsed.myfile


def start(myfile) -> None:
print(f'my file: {myfile}')


if __name__ == "__main__":
_parse()

I like this format because it can be called from command line

python coolapp.py "foofile"

Or used as an import

from coolapp import start
start("foofile")

PyTests

Happy paths are fairly straightforward. Using start() directly or implicitly via sys.argv

#test_coolapp.py
from coolapp import start, _parse
import sys

def test_coolapp():
""" Direct import of start """
start("myfile.txt")

def test_coolapp_sysargs():
""" Called through __main__ (eg. python coolapp.py myfile.txt) """
_parse(['myfile.txt'])

To be honest — I’d rather not have to import _parse and instead just test the logic under __name__ == "__main__" , but that requires too much work.

Invalid input is also important to test. Calling argparser with no args should produce a message like:

python coolapp.py

usage: start.py [-h] myfile
start.py: error: the following arguments are required: myfile

How do we test this? It’s non-trivial becaus argparser doesn’t raise a normal exception, it throws a SystemExit .

In typical python docs fashion, there’s a way in the py docs that is lacking a real-world example. It comes down to the capsys pytest fixture.

def test_coolapp_no_args(capsys):
""" ie. python coolapp.py """
with pytest.raises(SystemExit):
_parse([])
captured = capsys.readouterr()
assert "the following arguments are required: myfile" in captured.err

def test_coolapp_extra_args(capsys):
""" ie. python coolapp.py arg1 arg2 """
with pytest.raises(SystemExit):
_parse(['arg1', 'arg2'])
captured = capsys.readouterr()
assert "unrecognized arguments: arg2" in captured.err

Packaging

You may have noticed_parse(args=None)This is necessary for coolapp to be a script that gets installed from setup.py . As per this packaging guide and this SO discussion, our setup file can now reference the _parse method

#setup.py 

setup(
name='coolapp',
version='0.1',
packages=find_packages(),
entry_points={
'console_scripts': ['coolapp=package.coolapp:_parse
}
...
)

And then voila , upon pip install coolapp , we can then access it from anywhere.

coolapp foo.py
FileNotFoundError: [Errno 2] No such file or directory: 'foo.py'

--

--

Adam Hughes
Programmer’s Journey

Software Developer, Scientist, Muay Thai, hackDontSlack