CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/pypi-typer

Typer, build great CLIs. Easy to code. Based on Python type hints.

Overview
Eval results
Files

testing-support.mddocs/

Testing Support

Testing utilities for CLI applications built with Typer, providing a specialized test runner that integrates seamlessly with Typer applications.

Capabilities

CliRunner Class

A specialized test runner that extends Click's CliRunner with Typer-specific functionality for testing CLI applications.

class CliRunner(ClickCliRunner):
    def invoke(
        self,
        app: Typer,
        args: Optional[Union[str, Sequence[str]]] = None,
        input: Optional[Union[bytes, str, IO[Any]]] = None,
        env: Optional[Mapping[str, str]] = None,
        catch_exceptions: bool = True,
        color: bool = False,
        **extra: Any,
    ) -> Result:
        """
        Invoke a Typer application for testing.

        Parameters:
        - app: Typer application instance to test
        - args: Command line arguments as string or sequence
        - input: Input data to send to the application
        - env: Environment variables for the test
        - catch_exceptions: Whether to catch exceptions or let them propagate
        - color: Enable colored output
        - extra: Additional keyword arguments passed to Click's invoke

        Returns:
        Result object containing exit code, output, and exception information
        """

Result Object

The Result object returned by CliRunner.invoke() contains test execution information.

class Result:
    """
    Test execution result from CliRunner.

    Attributes:
    - exit_code: Exit code of the command (0 for success)
    - output: Standard output from the command
    - stderr: Standard error output (if available)
    - exception: Exception raised during execution (if any)
    - exc_info: Exception information tuple
    """
    exit_code: int
    output: str
    stderr: str
    exception: Optional[BaseException]
    exc_info: Optional[Tuple[Type[BaseException], BaseException, Any]]

Usage Examples

Basic Testing

import typer
from typer.testing import CliRunner

app = typer.Typer()

@app.command()
def hello(name: str):
    """Say hello to someone."""
    typer.echo(f"Hello {name}")

def test_hello():
    runner = CliRunner()
    result = runner.invoke(app, ["World"])
    assert result.exit_code == 0
    assert "Hello World" in result.output

def test_hello_missing_argument():
    runner = CliRunner()
    result = runner.invoke(app, [])
    assert result.exit_code != 0
    assert "Missing argument" in result.output

if __name__ == "__main__":
    test_hello()
    test_hello_missing_argument()
    print("All tests passed!")

Testing with Options

import typer
from typer.testing import CliRunner

app = typer.Typer()

@app.command()
def greet(
    name: str,
    count: int = typer.Option(1, "--count", "-c", help="Number of greetings"),
    formal: bool = typer.Option(False, "--formal", help="Use formal greeting")
):
    """Greet someone with options."""
    greeting = "Good day" if formal else "Hello"
    for _ in range(count):
        typer.echo(f"{greeting} {name}!")

def test_greet_basic():
    runner = CliRunner()
    result = runner.invoke(app, ["Alice"])
    assert result.exit_code == 0
    assert "Hello Alice!" in result.output

def test_greet_with_count():
    runner = CliRunner()
    result = runner.invoke(app, ["--count", "3", "Bob"])
    assert result.exit_code == 0
    assert result.output.count("Hello Bob!") == 3

def test_greet_formal():
    runner = CliRunner()
    result = runner.invoke(app, ["--formal", "Charlie"])
    assert result.exit_code == 0
    assert "Good day Charlie!" in result.output

def test_greet_short_options():
    runner = CliRunner()
    result = runner.invoke(app, ["-c", "2", "--formal", "David"])
    assert result.exit_code == 0
    assert result.output.count("Good day David!") == 2

if __name__ == "__main__":
    test_greet_basic()
    test_greet_with_count()
    test_greet_formal()
    test_greet_short_options()
    print("All tests passed!")

Testing Interactive Input

import typer
from typer.testing import CliRunner

app = typer.Typer()

@app.command()
def login():
    """Login with prompted credentials."""
    username = typer.prompt("Username")
    password = typer.prompt("Password", hide_input=True)
    
    if username == "admin" and password == "secret":
        typer.echo("Login successful!")
    else:
        typer.echo("Login failed!")
        raise typer.Exit(1)

def test_login_success():
    runner = CliRunner()
    result = runner.invoke(app, input="admin\nsecret\n")
    assert result.exit_code == 0
    assert "Login successful!" in result.output

def test_login_failure():
    runner = CliRunner()
    result = runner.invoke(app, input="user\nwrong\n")
    assert result.exit_code == 1
    assert "Login failed!" in result.output

if __name__ == "__main__":
    test_login_success()
    test_login_failure()
    print("All tests passed!")

Testing File Operations

import typer
from typer.testing import CliRunner
from pathlib import Path
import tempfile
import os

app = typer.Typer()

@app.command()
def process_file(
    input_file: Path = typer.Argument(..., exists=True),
    output_file: Path = typer.Option("output.txt", "--output", "-o")
):
    """Process a file."""
    content = input_file.read_text()
    processed = content.upper()
    output_file.write_text(processed)
    typer.echo(f"Processed {input_file} -> {output_file}")

def test_process_file():
    runner = CliRunner()
    
    with tempfile.TemporaryDirectory() as temp_dir:
        # Create test input file
        input_path = Path(temp_dir) / "input.txt"
        input_path.write_text("hello world")
        
        output_path = Path(temp_dir) / "result.txt"
        
        result = runner.invoke(app, [
            str(input_path),
            "--output", str(output_path)
        ])
        
        assert result.exit_code == 0
        assert "Processed" in result.output
        assert output_path.read_text() == "HELLO WORLD"

def test_process_file_not_found():
    runner = CliRunner()
    result = runner.invoke(app, ["nonexistent.txt"])
    assert result.exit_code != 0

if __name__ == "__main__":
    test_process_file()
    test_process_file_not_found()
    print("All tests passed!")

Testing Environment Variables

import typer
from typer.testing import CliRunner

app = typer.Typer()

@app.command()
def connect(
    host: str = typer.Option("localhost", envvar="DB_HOST"),
    port: int = typer.Option(5432, envvar="DB_PORT"),
    debug: bool = typer.Option(False, envvar="DEBUG")
):
    """Connect to database."""
    typer.echo(f"Connecting to {host}:{port}")
    if debug:
        typer.echo("Debug mode enabled")

def test_connect_defaults():
    runner = CliRunner()
    result = runner.invoke(app, [])
    assert result.exit_code == 0
    assert "Connecting to localhost:5432" in result.output
    assert "Debug mode" not in result.output

def test_connect_with_env():
    runner = CliRunner()
    result = runner.invoke(
        app, 
        [],
        env={"DB_HOST": "production", "DB_PORT": "3306", "DEBUG": "true"}
    )
    assert result.exit_code == 0
    assert "Connecting to production:3306" in result.output
    assert "Debug mode enabled" in result.output

def test_connect_cli_overrides_env():
    runner = CliRunner()
    result = runner.invoke(
        app,
        ["--host", "staging", "--port", "5433"],
        env={"DB_HOST": "production", "DB_PORT": "3306"}
    )
    assert result.exit_code == 0
    assert "Connecting to staging:5433" in result.output

if __name__ == "__main__":
    test_connect_defaults()
    test_connect_with_env()
    test_connect_cli_overrides_env()
    print("All tests passed!")

Testing Exception Handling

import typer
from typer.testing import CliRunner

app = typer.Typer()

@app.command()
def divide(a: float, b: float):
    """Divide two numbers."""
    if b == 0:
        typer.echo("Error: Cannot divide by zero!", err=True)
        raise typer.Exit(1)
    
    result = a / b
    typer.echo(f"{a} / {b} = {result}")

def test_divide_success():
    runner = CliRunner()
    result = runner.invoke(app, ["10", "2"])
    assert result.exit_code == 0
    assert "10.0 / 2.0 = 5.0" in result.output

def test_divide_by_zero():
    runner = CliRunner()
    result = runner.invoke(app, ["10", "0"])
    assert result.exit_code == 1
    assert "Cannot divide by zero!" in result.output

def test_divide_invalid_input():
    runner = CliRunner()
    result = runner.invoke(app, ["abc", "2"])
    assert result.exit_code != 0
    # Click will handle the type conversion error

if __name__ == "__main__":
    test_divide_success()
    test_divide_by_zero() 
    test_divide_invalid_input()
    print("All tests passed!")

Testing Sub-Applications

import typer
from typer.testing import CliRunner

app = typer.Typer()
users_app = typer.Typer()
app.add_typer(users_app, name="users")

@users_app.command()
def create(name: str):
    """Create a user."""
    typer.echo(f"Created user: {name}")

@users_app.command() 
def delete(name: str):
    """Delete a user."""
    typer.echo(f"Deleted user: {name}")

def test_users_create():
    runner = CliRunner()
    result = runner.invoke(app, ["users", "create", "alice"])
    assert result.exit_code == 0
    assert "Created user: alice" in result.output

def test_users_delete():
    runner = CliRunner()
    result = runner.invoke(app, ["users", "delete", "bob"])
    assert result.exit_code == 0
    assert "Deleted user: bob" in result.output

def test_users_help():
    runner = CliRunner()
    result = runner.invoke(app, ["users", "--help"])
    assert result.exit_code == 0
    assert "create" in result.output
    assert "delete" in result.output

if __name__ == "__main__":
    test_users_create()
    test_users_delete()
    test_users_help()
    print("All tests passed!")

Testing with Pytest

import pytest
import typer
from typer.testing import CliRunner

app = typer.Typer()

@app.command()
def hello(name: str, count: int = 1):
    """Say hello."""
    for _ in range(count):
        typer.echo(f"Hello {name}!")

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

class TestHelloCommand:
    def test_hello_basic(self, runner):
        result = runner.invoke(app, ["World"])
        assert result.exit_code == 0
        assert "Hello World!" in result.output
    
    def test_hello_with_count(self, runner):
        result = runner.invoke(app, ["Alice", "--count", "3"])
        assert result.exit_code == 0
        assert result.output.count("Hello Alice!") == 3
    
    @pytest.mark.parametrize("name,expected", [
        ("Bob", "Hello Bob!"),
        ("Charlie", "Hello Charlie!"),
        ("Diana", "Hello Diana!")
    ])
    def test_hello_names(self, runner, name, expected):
        result = runner.invoke(app, [name])
        assert result.exit_code == 0
        assert expected in result.output

Install with Tessl CLI

npx tessl i tessl/pypi-typer

docs

color-constants.md

core-application.md

file-handling.md

index.md

parameter-configuration.md

terminal-utilities.md

testing-support.md

tile.json