CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/pypi-types-click

Typing stubs for click - a command line interface creation kit for Python

Overview
Eval results
Files

testing-support.mddocs/

Testing Support

Built-in testing framework with CLI runner, result objects, and utilities for testing CLI applications in isolation with controlled input/output.

Capabilities

CLI Runner

Primary testing utility for invoking CLI commands in isolated environments.

class CliRunner:
    charset: str
    env: Mapping[str, str]
    echo_stdin: bool
    mix_stderr: bool

    def __init__(
        self,
        charset: str | None = None,
        env: Mapping[str, str] | None = None,
        echo_stdin: bool = False,
        mix_stderr: bool = True,
    ) -> None:
        """
        CLI test runner for isolated command execution.

        Parameters:
        - charset: Character encoding for input/output
        - env: Environment variables for test execution
        - echo_stdin: Echo stdin to stdout for debugging
        - mix_stderr: Mix stderr into stdout in results

        Usage:
        runner = click.testing.CliRunner()
        result = runner.invoke(my_command, ['--help'])
        """

    def invoke(
        self,
        cli: BaseCommand,
        args: str | Iterable[str] | None = None,
        input: bytes | str | IO[Any] | None = None,
        env: Mapping[str, str] | None = None,
        catch_exceptions: bool = True,
        color: bool = False,
        **extra: Any,
    ) -> Result:
        """
        Invoke a CLI command and return results.

        Parameters:
        - cli: Command or group to invoke
        - args: Command arguments (list or string)
        - input: Input data for stdin
        - env: Environment variables (merged with runner env)
        - catch_exceptions: Catch and store exceptions instead of raising
        - color: Enable colored output
        - **extra: Additional keyword arguments

        Returns:
        Result object with execution details

        Usage:
        # Basic command invocation
        result = runner.invoke(hello_command)
        
        # With arguments
        result = runner.invoke(hello_command, ['--name', 'World'])
        
        # With input
        result = runner.invoke(interactive_command, input='y\\n')
        
        # With environment
        result = runner.invoke(env_command, env={'DEBUG': '1'})
        """

    def isolated_filesystem(self) -> ContextManager[str]:
        """
        Create isolated temporary filesystem for testing.

        Returns:
        Context manager yielding temporary directory path

        Usage:
        with runner.isolated_filesystem():
            # Create test files
            with open('test.txt', 'w') as f:
                f.write('test content')
            
            # Run command that operates on files
            result = runner.invoke(process_command, ['test.txt'])
            assert result.exit_code == 0
        """

    def get_default_prog_name(self, cli: BaseCommand) -> str:
        """Get default program name for command."""

    def make_env(self, overrides: Mapping[str, str] | None = None) -> dict[str, str]:
        """Create environment dict with overrides."""

Test Result

Object containing the results of CLI command execution.

class Result:
    runner: CliRunner
    exit_code: int
    exception: Any
    exc_info: Any | None
    stdout_bytes: bytes
    stderr_bytes: bytes

    def __init__(
        self,
        runner: CliRunner,
        stdout_bytes: bytes,
        stderr_bytes: bytes,
        exit_code: int,
        exception: Any,
        exc_info: Any | None = None,
    ) -> None:
        """
        Result of CLI command execution.

        Attributes:
        - runner: CliRunner that produced this result
        - exit_code: Command exit code
        - exception: Exception that occurred (if any)
        - exc_info: Exception info tuple
        - stdout_bytes: Raw stdout bytes
        - stderr_bytes: Raw stderr bytes
        """

    @property
    def output(self) -> str:
        """
        Combined stdout output as string.

        Returns:
        Decoded stdout (and stderr if mixed)
        """

    @property
    def stdout(self) -> str:
        """
        Stdout output as string.

        Returns:
        Decoded stdout
        """

    @property
    def stderr(self) -> str:
        """
        Stderr output as string.

        Returns:
        Decoded stderr
        """

Input Stream Utilities

Utilities for creating test input streams.

def make_input_stream(input: bytes | str | IO[Any] | None, charset: str) -> BinaryIO:
    """
    Create input stream for testing.

    Parameters:
    - input: Input data
    - charset: Character encoding

    Returns:
    Binary input stream

    Usage:
    # Usually used internally by CliRunner
    stream = make_input_stream('test input\\n', 'utf-8')
    """

class EchoingStdin:
    """
    Stdin wrapper that echoes input to output for debugging.
    
    Used internally when CliRunner.echo_stdin is True.
    """

    def __init__(self, input: BinaryIO, output: BinaryIO) -> None: ...
    def read(self, n: int = ...) -> bytes: ...
    def readline(self, n: int = ...) -> bytes: ...
    def readlines(self) -> list[bytes]: ...

Testing Patterns

Basic Command Testing:

import click
from click.testing import CliRunner

@click.command()
@click.option('--count', default=1, help='Number of greetings')
@click.argument('name')
def hello(count, name):
    """Simple greeting command."""
    for _ in range(count):
        click.echo(f'Hello {name}!')

def test_hello_command():
    runner = CliRunner()
    
    # Test basic functionality
    result = runner.invoke(hello, ['World'])
    assert result.exit_code == 0
    assert 'Hello World!' in result.output
    
    # Test with options
    result = runner.invoke(hello, ['--count', '3', 'Alice'])
    assert result.exit_code == 0
    assert result.output.count('Hello Alice!') == 3
    
    # Test help
    result = runner.invoke(hello, ['--help'])
    assert result.exit_code == 0
    assert 'Simple greeting command' in result.output

Testing Interactive Commands:

@click.command()
def interactive_setup():
    """Interactive setup command."""
    name = click.prompt('Your name')
    age = click.prompt('Your age', type=int)
    if click.confirm('Save configuration?'):
        click.echo(f'Saved config for {name}, age {age}')
    else:
        click.echo('Configuration not saved')

def test_interactive_setup():
    runner = CliRunner()
    
    # Test with confirmations
    result = runner.invoke(interactive_setup, input='John\\n25\\ny\\n')
    assert result.exit_code == 0
    assert 'Saved config for John, age 25' in result.output
    
    # Test with rejection
    result = runner.invoke(interactive_setup, input='Jane\\n30\\nn\\n')
    assert result.exit_code == 0
    assert 'Configuration not saved' in result.output

Testing File Operations:

@click.command()
@click.argument('filename', type=click.Path(exists=True))
def process_file(filename):
    """Process a file."""
    with open(filename, 'r') as f:
        content = f.read()
    
    lines = len(content.splitlines())
    click.echo(f'File has {lines} lines')

def test_process_file():
    runner = CliRunner()
    
    with runner.isolated_filesystem():
        # Create test file
        with open('test.txt', 'w') as f:
            f.write('line 1\\nline 2\\nline 3\\n')
        
        # Test command
        result = runner.invoke(process_file, ['test.txt'])
        assert result.exit_code == 0
        assert 'File has 3 lines' in result.output
        
        # Test missing file
        result = runner.invoke(process_file, ['missing.txt'])
        assert result.exit_code != 0

Testing Environment Variables:

@click.command()
def env_command():
    """Command that uses environment variables."""
    debug = os.environ.get('DEBUG', 'false').lower() == 'true'
    if debug:
        click.echo('Debug mode enabled')
    else:
        click.echo('Debug mode disabled')

def test_env_command():
    runner = CliRunner()
    
    # Test without environment
    result = runner.invoke(env_command)
    assert 'Debug mode disabled' in result.output
    
    # Test with environment
    result = runner.invoke(env_command, env={'DEBUG': 'true'})
    assert 'Debug mode enabled' in result.output

Testing Exception Handling:

@click.command()
@click.argument('number', type=int)
def divide_by_number(number):
    """Divide 100 by the given number."""
    if number == 0:
        raise click.BadParameter('Cannot divide by zero')
    
    result = 100 / number
    click.echo(f'100 / {number} = {result}')

def test_divide_by_number():
    runner = CliRunner()
    
    # Test normal operation
    result = runner.invoke(divide_by_number, ['5'])
    assert result.exit_code == 0
    assert '100 / 5 = 20.0' in result.output
    
    # Test error handling
    result = runner.invoke(divide_by_number, ['0'])
    assert result.exit_code != 0
    assert 'Cannot divide by zero' in result.output
    
    # Test with catch_exceptions=False to see actual exception
    with pytest.raises(click.BadParameter):
        runner.invoke(divide_by_number, ['0'], catch_exceptions=False)

Testing Groups and Subcommands:

@click.group()
def cli():
    """Main CLI group."""
    pass

@cli.command()
def status():
    """Show status."""
    click.echo('Status: OK')

@cli.command()
@click.argument('name')
def greet(name):
    """Greet someone."""
    click.echo(f'Hello {name}!')

def test_cli_group():
    runner = CliRunner()
    
    # Test group help
    result = runner.invoke(cli, ['--help'])
    assert result.exit_code == 0
    assert 'Main CLI group' in result.output
    
    # Test subcommand
    result = runner.invoke(cli, ['status'])
    assert result.exit_code == 0
    assert 'Status: OK' in result.output
    
    # Test subcommand with args
    result = runner.invoke(cli, ['greet', 'World'])
    assert result.exit_code == 0
    assert 'Hello World!' in result.output

Testing with Fixtures:

import pytest

@pytest.fixture
def runner():
    """CLI runner fixture."""
    return CliRunner()

@pytest.fixture
def temp_config(runner):
    """Temporary config file fixture."""
    with runner.isolated_filesystem():
        config = {'host': 'localhost', 'port': 8080}
        with open('config.json', 'w') as f:
            json.dump(config, f)
        yield 'config.json'

def test_with_fixtures(runner, temp_config):
    result = runner.invoke(load_config_command, [temp_config])
    assert result.exit_code == 0

Install with Tessl CLI

npx tessl i tessl/pypi-types-click

docs

commands-groups.md

context-management.md

exception-handling.md

formatting.md

index.md

parameter-types.md

parameters.md

terminal-ui.md

testing-support.md

tile.json