Typer, build great CLIs. Easy to code. Based on Python type hints.
Testing utilities for CLI applications built with Typer, providing a specialized test runner that integrates seamlessly with Typer applications.
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
"""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]]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!")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!")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!")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!")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!")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!")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!")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.outputInstall with Tessl CLI
npx tessl i tessl/pypi-typer