I am trying to run a test case for a python script. Test case is successful when I don’t use Click decorator and argument in python script method.
But when I am using it, it gives SystemExit: 0
.
commands.py:
import sys import click @click.command() @click.argument('bar_info', nargs=-1) def get_all_foos(bar_info): print("bars_to_retain --> {}".format(bar_info[1])) print("bars_prefix --> {}".format(bar_info[0])) # do something with bar_info return "result"
tests/test_commands.py:
import sys import pytest import commands from click.testing import CliRunner def test_foo_list(): response = commands.get_all_foos(["ctm", "10"]) print("response is --> {}".format(response))
When I run the test case:
pytest -rA tests/test_commands.py
then the test fails with:
FAILED tests/test_commands.py::test_foo_list - SystemExit: 0
The full output is:
============================= test session starts ============================== platform darwin -- Python 3.10.3, pytest-7.1.2, pluggy-1.0.0 rootdir: ... collected 1 item tests/test_commands.py F [100%] =================================== FAILURES =================================== ________________________________ test_foo_list _________________________________ self = <Command get-all-foos>, args = [], prog_name = 'pytest' complete_var = None, standalone_mode = True, windows_expand_args = True extra = {}, ctx = <click.core.Context object at 0x107585d20>, rv = 'result' def main( self, args: t.Optional[t.Sequence[str]] = None, prog_name: t.Optional[str] = None, complete_var: t.Optional[str] = None, standalone_mode: bool = True, windows_expand_args: bool = True, **extra: t.Any, ) -> t.Any: """This is the way to invoke a script with all the bells and whistles as a command line application. This will always terminate the application after a call. If this is not wanted, ``SystemExit`` needs to be caught. This method is also available by directly calling the instance of a :class:`Command`. :param args: the arguments that should be used for parsing. If not provided, ``sys.argv[1:]`` is used. :param prog_name: the program name that should be used. By default the program name is constructed by taking the file name from ``sys.argv[0]``. :param complete_var: the environment variable that controls the bash completion support. The default is ``"_<prog_name>_COMPLETE"`` with prog_name in uppercase. :param standalone_mode: the default behavior is to invoke the script in standalone mode. Click will then handle exceptions and convert them into error messages and the function will never return but shut down the interpreter. If this is set to `False` they will be propagated to the caller and the return value of this function is the return value of :meth:`invoke`. :param windows_expand_args: Expand glob patterns, user dir, and env vars in command line args on Windows. :param extra: extra keyword arguments are forwarded to the context constructor. See :class:`Context` for more information. .. versionchanged:: 8.0.1 Added the ``windows_expand_args`` parameter to allow disabling command line arg expansion on Windows. .. versionchanged:: 8.0 When taking arguments from ``sys.argv`` on Windows, glob patterns, user dir, and env vars are expanded. .. versionchanged:: 3.0 Added the ``standalone_mode`` parameter. """ if args is None: args = sys.argv[1:] if os.name == "nt" and windows_expand_args: args = _expand_args(args) else: args = list(args) if prog_name is None: prog_name = _detect_program_name() # Process shell completion requests and exit early. self._main_shell_completion(extra, prog_name, complete_var) try: try: with self.make_context(prog_name, args, **extra) as ctx: rv = self.invoke(ctx) if not standalone_mode: return rv # it's not safe to `ctx.exit(rv)` here! # note that `rv` may actually contain data like "1" which # has obvious effects # more subtle case: `rv=[None, None]` can come out of # chained commands which all returned `None` -- so it's not # even always obvious that `rv` indicates success/failure # by its truthiness/falsiness > ctx.exit() lib/python3.10/site-packages/click/core.py:1065: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ self = <click.core.Context object at 0x107585d20>, code = 0 def exit(self, code: int = 0) -> "te.NoReturn": """Exits the application with a given exit code.""" > raise Exit(code) E click.exceptions.Exit: 0 lib/python3.10/site-packages/click/core.py:687: Exit During handling of the above exception, another exception occurred: def test_foo_list(): > response = commands.get_all_foos(["ctm", "10"]) tests/test_commands.py:9: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ lib/python3.10/site-packages/click/core.py:1130: in __call__ return self.main(*args, **kwargs) _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ self = <Command get-all-foos>, args = [], prog_name = 'pytest' complete_var = None, standalone_mode = True, windows_expand_args = True extra = {}, ctx = <click.core.Context object at 0x107585d20>, rv = 'result' def main( self, args: t.Optional[t.Sequence[str]] = None, prog_name: t.Optional[str] = None, complete_var: t.Optional[str] = None, standalone_mode: bool = True, windows_expand_args: bool = True, **extra: t.Any, ) -> t.Any: """This is the way to invoke a script with all the bells and whistles as a command line application. This will always terminate the application after a call. If this is not wanted, ``SystemExit`` needs to be caught. This method is also available by directly calling the instance of a :class:`Command`. :param args: the arguments that should be used for parsing. If not provided, ``sys.argv[1:]`` is used. :param prog_name: the program name that should be used. By default the program name is constructed by taking the file name from ``sys.argv[0]``. :param complete_var: the environment variable that controls the bash completion support. The default is ``"_<prog_name>_COMPLETE"`` with prog_name in uppercase. :param standalone_mode: the default behavior is to invoke the script in standalone mode. Click will then handle exceptions and convert them into error messages and the function will never return but shut down the interpreter. If this is set to `False` they will be propagated to the caller and the return value of this function is the return value of :meth:`invoke`. :param windows_expand_args: Expand glob patterns, user dir, and env vars in command line args on Windows. :param extra: extra keyword arguments are forwarded to the context constructor. See :class:`Context` for more information. .. versionchanged:: 8.0.1 Added the ``windows_expand_args`` parameter to allow disabling command line arg expansion on Windows. .. versionchanged:: 8.0 When taking arguments from ``sys.argv`` on Windows, glob patterns, user dir, and env vars are expanded. .. versionchanged:: 3.0 Added the ``standalone_mode`` parameter. """ if args is None: args = sys.argv[1:] if os.name == "nt" and windows_expand_args: args = _expand_args(args) else: args = list(args) if prog_name is None: prog_name = _detect_program_name() # Process shell completion requests and exit early. self._main_shell_completion(extra, prog_name, complete_var) try: try: with self.make_context(prog_name, args, **extra) as ctx: rv = self.invoke(ctx) if not standalone_mode: return rv # it's not safe to `ctx.exit(rv)` here! # note that `rv` may actually contain data like "1" which # has obvious effects # more subtle case: `rv=[None, None]` can come out of # chained commands which all returned `None` -- so it's not # even always obvious that `rv` indicates success/failure # by its truthiness/falsiness ctx.exit() except (EOFError, KeyboardInterrupt): echo(file=sys.stderr) raise Abort() from None except ClickException as e: if not standalone_mode: raise e.show() sys.exit(e.exit_code) except OSError as e: if e.errno == errno.EPIPE: sys.stdout = t.cast(t.TextIO, PacifyFlushWrapper(sys.stdout)) sys.stderr = t.cast(t.TextIO, PacifyFlushWrapper(sys.stderr)) sys.exit(1) else: raise except Exit as e: if standalone_mode: > sys.exit(e.exit_code) E SystemExit: 0 lib/python3.10/site-packages/click/core.py:1083: SystemExit ----------------------------- Captured stdout call ----------------------------- bars_to_retain --> 10 bars_prefix --> ctm =========================== short test summary info ============================ FAILED tests/test_commands.py::test_foo_list - SystemExit: 0 ============================== 1 failed in 0.15s ===============================
Advertisement
Answer
Don’t run click
commands directly; they will trigger sys.exit(0)
when done, which is the normal and correct way to end a command-line tool, but not very useful when trying to test the command.
Instead, follow the Testing Click Applications chapter and use CliRunner()
object to run your commands:
from click.testing import CliRunner # ... def test_foo_list(): runner = CliRunner() result = runner.invoke(commands.get_all_foos, ["ctm", "10"]) response = result.return_value
Note that I passed in a list of string arguments; click
will parse those into the correct structure.
I assigned result.return_value
to response
here, but know that that’ll be None
; click commands are not supposed to return anything really, because they normally would communicate with the user via the terminal or by interacting with the filesystem, etc.:
________________________________ test_foo_list _________________________________ ----------------------------- Captured stdout call ----------------------------- response is --> None =========================== short test summary info ============================ PASSED tests/test_commands.py::test_foo_list
You could perhaps use print()
or click.echo()
to write something to stdout
and then test for that output via result.stdout
, though.