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, as argparse handles this case already.