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!