Dead Simple: PyTest and ArgParse
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'