Nick George
all/

Using Python's argparse for tests

First published: March 11, 2023
Last updated: March 11, 2023

Python’s argparse library is great for building simple command line interfaces (CLIs) around your scripts. When writing tests, it is nice to test the CLI entrypoints as well as your typical unit and integration tests, and argparse makes this easy. I often start with the following pattern:

import argparse
import sys


def cli(args=sys.argv[1:]):
    parser = argparse.ArgumentParser(
        prog="A test CLI", description="For testing a CLI", help="Help goes here"
    )

    parser.add_argument("--foo", type=int)
    return parser.parse_args(args)
    # ...

As explained in the docs, you can pass arguments directly to parse_args using a list. This function uses the sys.argv list if nothing is supplied, so:

# in a test
prg_args = cli(["--foo", 5])
assert prg_args.foo == 5  # true

# in production, this just uses sys.argv[1:]
prg_args = cli()

This pattern makes it easy to write unit tests for any guards or checks you want to include when receiving/parsing user provided arguments, such as checking if a file exists:

import argparse
import os
import sys


def cli(args=sys.argv[1:]):
    parser = argparse.ArgumentParser(prog="A test CLI")

    parser.add_argument("--path", type=str, required=True)
    args = parser.parse_args(args)
    if not os.path.exists(args.path):
        print(f"[ERROR] Path {args.path} doesn't exist")
        raise FileNotFoundError
    return parser.parse_args(args)

Now you can write a unit test:

import pytest

import testcli

def test_bad_path():
    with pytest.raises(FileNotFoundError):
        testcli.cli(["--path", "doesnt/exist.md"])

This also makes it easy to run integration tests with input just like users would on the command line. If you wrap your script in some main like function, that function can also take args=sys.argv[1:] and pass args to cli(). Then you could write integration tests:

# main file
def main(args=sys.argv[1:]):
    prg_args = cli(args)
    # Do things...


# integration test:
def big_integration_test():
    test_input = ["--option", "argument 1", "--anotheropt", "another arg"]
    results = main(test_input)
    # ... your test assertions

Your primary entrypoint would be main() with no arguments, which would use the default argument(s) of sys.argv[1:]. I’ve found this pattern makes it easy to write and test my Python CLI’s, hopefully you will too!