diff --git a/docs/testing.md b/docs/testing.md index 6d437a2b0..d37ba23f4 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -1,28 +1,30 @@ # Testing Click Applications -```{eval-rst} .. currentmodule:: click.testing -``` -Click provides the {ref}`click.testing ` module to help you invoke command line applications and check their behavior. +Click provides the :ref:`click.testing ` module to help you invoke +command line applications and check their behavior. -These tools should only be used for testing since they change -the entire interpreter state for simplicity. They are not thread-safe! +These tools should only be used for testing since they change the entire +interpreter state for simplicity. They are not thread-safe. -The examples use [pytest](https://docs.pytest.org/en/stable/) style tests. +The examples use `pytest `\_ style tests. -```{contents} +.. contents:: :depth: 1 :local: true -``` + +--- ## Basic Example The key pieces are: - - {class}`CliRunner` - used to invoke commands as command line scripts. - - {class}`Result` - returned from {meth}`CliRunner.invoke`. Captures output data, exit code, optional exception, and captures the output as bytes and binary data. -```{code-block} python +- :class:`CliRunner` — used to invoke commands as command line scripts. +- :class:`Result` — returned from :meth:`CliRunner.invoke`. Captures output + data, exit code, optional exception, and output as text or binary data. + +.. code-block:: python :caption: hello.py import click @@ -30,169 +32,181 @@ import click @click.command() @click.argument('name') def hello(name): - click.echo(f'Hello {name}!') -``` +click.echo(f"Hello {name}!") -```{code-block} python +.. code-block:: python :caption: test_hello.py from click.testing import CliRunner from hello import hello def test_hello_world(): - runner = CliRunner() - result = runner.invoke(hello, ['Peter']) - assert result.exit_code == 0 - assert result.output == 'Hello Peter!\n' -``` +runner = CliRunner() +result = runner.invoke(hello, ["Peter"]) +assert result.exit_code == 0 +assert result.output == "Hello Peter!\n" + +--- ## Subcommands -A subcommand name must be specified in the `args` parameter {meth}`CliRunner.invoke`: +A subcommand name must be specified in the `args` parameter of +:meth:`CliRunner.invoke`: -```{code-block} python +.. code-block:: python :caption: sync.py import click @click.group() -@click.option('--debug/--no-debug', default=False) +@click.option("--debug/--no-debug", default=False) def cli(debug): - click.echo(f"Debug mode is {'on' if debug else 'off'}") +click.echo(f"Debug mode is {'on' if debug else 'off'}") @cli.command() def sync(): - click.echo('Syncing') -``` +click.echo("Syncing") -```{code-block} python +.. code-block:: python :caption: test_sync.py from click.testing import CliRunner from sync import cli def test_sync(): - runner = CliRunner() - result = runner.invoke(cli, ['--debug', 'sync']) - assert result.exit_code == 0 - assert 'Debug mode is on' in result.output - assert 'Syncing' in result.output -``` +runner = CliRunner() +result = runner.invoke(cli, ["--debug", "sync"]) +assert result.exit_code == 0 +assert "Debug mode is on" in result.output +assert "Syncing" in result.output + +--- ## Context Settings -Additional keyword arguments passed to {meth}`CliRunner.invoke` will be used to construct the initial {class}`Context object `. -For example, setting a fixed terminal width equal to 60: +Additional keyword arguments passed to :meth:`CliRunner.invoke` are used to +construct the initial :class:`click.Context` object. -```{code-block} python +A common use case is setting a fixed terminal width to make help text and +formatted output deterministic during tests. + +.. code-block:: python :caption: sync.py import click @click.group() -def cli(): - pass +@click.option("--debug/--no-debug", default=False) +def cli(debug): +click.echo(f"Debug mode is {'on' if debug else 'off'}") @cli.command() def sync(): - click.echo('Syncing') -``` +click.echo("Syncing") -```{code-block} python +.. code-block:: python :caption: test_sync.py from click.testing import CliRunner from sync import cli -def test_sync(): - runner = CliRunner() - result = runner.invoke(cli, ['sync'], terminal_width=60) - assert result.exit_code == 0 - assert 'Debug mode is on' in result.output - assert 'Syncing' in result.output -``` +def test_sync_with_context_settings(): +runner = CliRunner() +result = runner.invoke( +cli, +["--debug", "sync"], +terminal_width=60, +) + + assert result.exit_code == 0 + assert "Debug mode is on" in result.output + assert "Syncing" in result.output + +Setting `terminal_width` affects how Click formats output (for example, +line wrapping in `--help` text), but does not change command behavior. + +--- ## File System Isolation -The {meth}`CliRunner.isolated_filesystem` context manager sets the current working directory to a new, empty folder. +The :meth:`CliRunner.isolated_filesystem` context manager sets the current +working directory to a new, empty folder. -```{code-block} python +.. code-block:: python :caption: cat.py import click @click.command() -@click.argument('f', type=click.File()) +@click.argument("f", type=click.File()) def cat(f): - click.echo(f.read()) -``` +click.echo(f.read()) -```{code-block} python +.. code-block:: python :caption: test_cat.py from click.testing import CliRunner from cat import cat def test_cat(): - runner = CliRunner() - with runner.isolated_filesystem(): - with open('hello.txt', 'w') as f: - f.write('Hello World!') +runner = CliRunner() +with runner.isolated_filesystem(): +with open("hello.txt", "w") as f: +f.write("Hello World!") - result = runner.invoke(cat, ['hello.txt']) - assert result.exit_code == 0 - assert result.output == 'Hello World!\n' -``` + result = runner.invoke(cat, ["hello.txt"]) + assert result.exit_code == 0 + assert result.output == "Hello World!\n" -Pass in a path to control where the temporary directory is created. -In this case, the directory will not be removed by Click. Its useful -to integrate with a framework like Pytest that manages temporary files. +You can pass a path to control where the temporary directory is created. +In this case, the directory will not be removed by Click. This is useful +when integrating with a framework like pytest that manages temporary files. -```{code-block} python +.. code-block:: python :caption: test_cat.py from click.testing import CliRunner from cat import cat def test_cat_with_path_specified(): - runner = CliRunner() - with runner.isolated_filesystem('~/test_folder'): - with open('hello.txt', 'w') as f: - f.write('Hello World!') +runner = CliRunner() +with runner.isolated_filesystem("~/test_folder"): +with open("hello.txt", "w") as f: +f.write("Hello World!") - result = runner.invoke(cat, ['hello.txt']) - assert result.exit_code == 0 - assert result.output == 'Hello World!\n' -``` + result = runner.invoke(cat, ["hello.txt"]) + assert result.exit_code == 0 + assert result.output == "Hello World!\n" + +--- ## Input Streams -The test wrapper can provide input data for the input stream (stdin). This is very useful for testing prompts. +The test runner can provide input data for the input stream (stdin). This +is useful for testing prompts. -```{code-block} python +.. code-block:: python :caption: prompt.py import click @click.command() -@click.option('--foo', prompt=True) +@click.option("--foo", prompt=True) def prompt(foo): - click.echo(f"foo={foo}") -``` +click.echo(f"foo={foo}") -```{code-block} python +.. code-block:: python :caption: test_prompt.py from click.testing import CliRunner from prompt import prompt def test_prompts(): - runner = CliRunner() - result = runner.invoke(prompt, input='wau wau\n') - assert not result.exception - assert result.output == 'Foo: wau wau\nfoo=wau wau\n' -``` - -Prompts will be emulated so they write the input data to -the output stream as well. If hidden input is expected then this -does not happen. +runner = CliRunner() +result = runner.invoke(prompt, input="wau wau\n") + + assert not result.exception + assert result.output == "Foo: wau wau\nfoo=wau wau\n" + +Prompts are emulated by echoing input back to the output stream. If hidden +input is expected (for example, passwords), the input will not be echoed.