When you are creating command line applications in Python, you probably heard of argparse, which is a great library for exactly doing this, and it is even included in Python’s standard library.
Imagine you have created the following argparse application:
<main.py>
import argparse
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--name', required=True)
args = parser.parse_args()
print(f'Hello {args.name}')
if __name__ == '__main__':
sys.exit(main())
Looks straightforward, works great, but at one point, you certainly want to add tests for it, right?
The problem
argparse.parse_args()
is reading the arguments (e.g. --name
) from sys.argv
,
so you cannot set some test values directly.
How would you test the code?
A quick search on Google showed a couple of solutions,
from patching parse_args
over to patching values into sys.argv
and even more frightening ways.
Actually, you could also create a “black box test” by using os.system("<call your app>")
to run your app.
Let’s try patching sys.argv
As mentioned you can patch sys.argv
.
<test_main.py>
from unittest.mock import patch
from main import main
def test_main(capsys):
with patch("sys.argv", ["main", "--name", "Jürgen"]):
main()
captured = capsys.readouterr()
assert captured.out == "Hello Jürgen\n"
This certainly works, but there must be a better way, right?
Less patching, more passing in
Indeed, you can rewrite the code as following…
<main.py>
import argparse
import sys
def main(argv=None):
parser = argparse.ArgumentParser()
parser.add_argument('--name', required=True)
args = parser.parse_args(argv)
print(f'Hello {args.name}')
if __name__ == '__main__':
sys.exit(main())
Now, instead of relying on sys.argv
,
you can also pass in arguments to your main
function directly.
This way, you can rewrite your test as following:
<test_main.py>
from main import main
def test_main_even_simpler(capsys):
main(["--name", "Jürgen"])
captured = capsys.readouterr()
assert captured.out == "Hello Jürgen\n"
There is no more need to patch sys.argv
!
Thank you
Thanks to Anthony Sottile, who showed me this “trick” sometimes back in 2020.
Updates
2021.11.30
Thanks to Anthony Sottile I now know that “You don’t even need the hackery with
sys.argv[1:]
” and so I was able to make the above code even simpler.Thanks to my friend Miroslav Šedivý and - once more - Anthony Sottile the code block (
if sys.argv is None
) is now also gone, asargparse
handles this case already.