Package: clirunner
CliRunnerA test helper for invoking and testing command line interfaces (CLIs). This is adapted from theClickCliRunnerbut modified to work with non-Click scripts, such as those usingargparsefor parsing command line arguments.Installationpython3 -m pip install clirunnerSource CodeThe source code is available onGitHub.MotivationI write a lot of Python command line tools. I usually reach for Click to build the CLI but sometimes will use argparse or even just manualsys.argvparsing for simple scripts or where I do not want to introduce a dependency on Click. Click provides a very usefulCliRunnerfor testing CLIs, but it only works with Click applications. This project is a derivative of Click's CliRunner that works with non-Click scripts. The API is the same as Click's CliRunner, so it should be easy to switch between the two if you later refactor to use Click.Supported PlatformsTested on macOS, Ubuntu Linux, and Windows using "*-latest" GitHub Workflow runners with Python 3.9 - 3.12.DocumentationFull documentation is availablehere.Basic TestingCliRunner can invoke your CLI's main function as a command line script. The CliRunner.invoke() method runs the command line script in isolation and captures the output as both bytes and binary data.The return value is a Result object, which has the captured output data, exit code, and optional exception attached:hello.py"""Simple CLI """importargparsedefhello():"""Print Hello World"""argp=argparse.ArgumentParser(description="Print Hello World")argp.add_argument("-n","--name",help="Name to greet")args=argp.parse_args()print(f"Hello{args.nameor'World'}!")if__name__=="__main__":hello()test_hello.py"""Test hello.py"""fromhelloimporthellofromclirunnerimportCliRunnerdeftest_hello_world():runner=CliRunner()result=runner.invoke(hello,["--name","Peter"])assertresult.exit_code==0assertresult.output=="Hello Peter!\n"Note thatresult.outputwill contain the combined output ofstdoutandstderr. If you want to capturestdoutandstderrseparately, useresult.stdoutandresult.stderr.File System IsolationFor basic command line tools with file system operations, theCliRunner.isolated_filesystem()method is useful for setting the current working directory to a new, empty folder.cat.py"""Simple cat program for testing isolated file system"""importargparsedefcat():argp=argparse.ArgumentParser()argp.add_argument("file",type=argparse.FileType("r"))args=argp.parse_args()print(args.file.read(),end="")if__name__=="__main__":cat()test_cat.py"""Test cat.py example."""fromcatimportcatfromclirunnerimportCliRunnerdeftest_cat():runner=CliRunner()withrunner.isolated_filesystem():withopen("hello.txt","w")asf:f.write("Hello World!\n")result=runner.invoke(cat,["hello.txt"])assertresult.exit_code==0assertresult.output=="Hello World!\n"Passtemp_dirto control where the temporary directory is created. The directory will not be removed byCliRunnerin this case. This is useful to integrate with a framework like Pytest that manages temporary files.deftest_keep_dir(tmp_path):runner=CliRunner()withrunner.isolated_filesystem(temp_dir=tmp_path)astd:...Input StreamsThe test wrapper can also be used to provide input data for the input stream (stdin). This is very useful for testing prompts, for instance:prompt.py"""Simple example for testing input streams"""defprompt():foo=input("Foo: ")print(f"foo ={foo}")if__name__=="__main__":prompt()test_prompt.py"""Test prompt.py example"""frompromptimportpromptfromclirunnerimportCliRunnerdeftest_prompts():runner=CliRunner()result=runner.invoke(prompt,input="wau wau\n")assertnotresult.exception# note: unlike click.CliRunner, clirunner.CliRunner does not echo the inputassert"foo = wau wau\n"inresult.outputNote that the input will not be echoed to the output stream. This is different from the behavior of theinput()function, which does echo the input and from click'sprompt()function, which also echo's the input when under test.Environment Variable IsolationTheCliRunner.invoke()method can also be used to set environment variables for the command line script. This is useful for testing command line tools that use environment variables for configuration.hello_env.py"""Say hello to the world, shouting if desired."""importosdefhello():"""Say hello to the world, shouting if desired."""ifos.getenv("SHOUT")=="1":print("HELLO WORLD!")else:print("Hello World!")if__name__=="__main__":hello()test_hello_env.py"""Test hello2.py showing how to set environment variables for testing."""fromhello_envimporthellofromclirunnerimportCliRunnerdeftest_hello():"""Test hello2.py"""runner=CliRunner()result=runner.invoke(hello)assertresult.exit_code==0assertresult.output=="Hello World!\n"deftest_hello_shouting():"""Test hello2.py"""runner=CliRunner()result=runner.invoke(hello,env={"SHOUT":"1"})assertresult.exit_code==0assertresult.output=="HELLO WORLD!\n"Handling ExceptionsNormally theCliRunner.invoke()method will catch exceptions in the CLI under test. If an exception is raised, it will be available via theResult.exceptionproperty. This can be disabled by passingcatch_exceptions=Falseto theCliRunner.invoke()method.raise_exception.py"""Simple script that raises an exception"""defraise_exception():"""Raises a ValueError exception"""raiseValueError("Exception raised")if__name__=="__main__":raise_exception()test_raise_exception.py"""Test raise_exception.py"""importpytestfromraise_exceptionimportraise_exceptionfromclirunnerimportCliRunnerdeftest_exception_caught():"""CliRunner normally catches exceptions"""runner=CliRunner()result=runner.invoke(raise_exception)# exit code will not be 0 if exception is raisedassertresult.exit_code!=0assertisinstance(result.exception,ValueError)deftest_exception_not_caught():"""CliRunner can be configured to not catch exceptions"""runner=CliRunner()withpytest.raises(ValueError):runner.invoke(raise_exception,catch_exceptions=False)Testing Click ApplicationsDo not useclirunner.CliRunnerto test applications built withClick,Typer, or another Click derivative. Instead, use Click's built-inCliRunnerorTyper's equivalent.clirunner.CliRunneris designed for testing non-Click scripts such as those usingargparseor manualsys.argvargument parsing. It has also been tested withpydantic-argparse,clipstick, andtyro.LicenseCliRunner is a derivative work of Click's CliRunner, and so it is licensed under the same BSD 3-clause license as Click. See theLICENSEandLICENSE.Clickfiles for details.