or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

color-constants.mdcore-application.mdfile-handling.mdindex.mdparameter-configuration.mdterminal-utilities.mdtesting-support.md

testing-support.mddocs/

0

# Testing Support

1

2

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

3

4

## Capabilities

5

6

### CliRunner Class

7

8

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

9

10

```python { .api }

11

class CliRunner(ClickCliRunner):

12

def invoke(

13

self,

14

app: Typer,

15

args: Optional[Union[str, Sequence[str]]] = None,

16

input: Optional[Union[bytes, str, IO[Any]]] = None,

17

env: Optional[Mapping[str, str]] = None,

18

catch_exceptions: bool = True,

19

color: bool = False,

20

**extra: Any,

21

) -> Result:

22

"""

23

Invoke a Typer application for testing.

24

25

Parameters:

26

- app: Typer application instance to test

27

- args: Command line arguments as string or sequence

28

- input: Input data to send to the application

29

- env: Environment variables for the test

30

- catch_exceptions: Whether to catch exceptions or let them propagate

31

- color: Enable colored output

32

- extra: Additional keyword arguments passed to Click's invoke

33

34

Returns:

35

Result object containing exit code, output, and exception information

36

"""

37

```

38

39

### Result Object

40

41

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

42

43

```python { .api }

44

class Result:

45

"""

46

Test execution result from CliRunner.

47

48

Attributes:

49

- exit_code: Exit code of the command (0 for success)

50

- output: Standard output from the command

51

- stderr: Standard error output (if available)

52

- exception: Exception raised during execution (if any)

53

- exc_info: Exception information tuple

54

"""

55

exit_code: int

56

output: str

57

stderr: str

58

exception: Optional[BaseException]

59

exc_info: Optional[Tuple[Type[BaseException], BaseException, Any]]

60

```

61

62

## Usage Examples

63

64

### Basic Testing

65

66

```python

67

import typer

68

from typer.testing import CliRunner

69

70

app = typer.Typer()

71

72

@app.command()

73

def hello(name: str):

74

"""Say hello to someone."""

75

typer.echo(f"Hello {name}")

76

77

def test_hello():

78

runner = CliRunner()

79

result = runner.invoke(app, ["World"])

80

assert result.exit_code == 0

81

assert "Hello World" in result.output

82

83

def test_hello_missing_argument():

84

runner = CliRunner()

85

result = runner.invoke(app, [])

86

assert result.exit_code != 0

87

assert "Missing argument" in result.output

88

89

if __name__ == "__main__":

90

test_hello()

91

test_hello_missing_argument()

92

print("All tests passed!")

93

```

94

95

### Testing with Options

96

97

```python

98

import typer

99

from typer.testing import CliRunner

100

101

app = typer.Typer()

102

103

@app.command()

104

def greet(

105

name: str,

106

count: int = typer.Option(1, "--count", "-c", help="Number of greetings"),

107

formal: bool = typer.Option(False, "--formal", help="Use formal greeting")

108

):

109

"""Greet someone with options."""

110

greeting = "Good day" if formal else "Hello"

111

for _ in range(count):

112

typer.echo(f"{greeting} {name}!")

113

114

def test_greet_basic():

115

runner = CliRunner()

116

result = runner.invoke(app, ["Alice"])

117

assert result.exit_code == 0

118

assert "Hello Alice!" in result.output

119

120

def test_greet_with_count():

121

runner = CliRunner()

122

result = runner.invoke(app, ["--count", "3", "Bob"])

123

assert result.exit_code == 0

124

assert result.output.count("Hello Bob!") == 3

125

126

def test_greet_formal():

127

runner = CliRunner()

128

result = runner.invoke(app, ["--formal", "Charlie"])

129

assert result.exit_code == 0

130

assert "Good day Charlie!" in result.output

131

132

def test_greet_short_options():

133

runner = CliRunner()

134

result = runner.invoke(app, ["-c", "2", "--formal", "David"])

135

assert result.exit_code == 0

136

assert result.output.count("Good day David!") == 2

137

138

if __name__ == "__main__":

139

test_greet_basic()

140

test_greet_with_count()

141

test_greet_formal()

142

test_greet_short_options()

143

print("All tests passed!")

144

```

145

146

### Testing Interactive Input

147

148

```python

149

import typer

150

from typer.testing import CliRunner

151

152

app = typer.Typer()

153

154

@app.command()

155

def login():

156

"""Login with prompted credentials."""

157

username = typer.prompt("Username")

158

password = typer.prompt("Password", hide_input=True)

159

160

if username == "admin" and password == "secret":

161

typer.echo("Login successful!")

162

else:

163

typer.echo("Login failed!")

164

raise typer.Exit(1)

165

166

def test_login_success():

167

runner = CliRunner()

168

result = runner.invoke(app, input="admin\nsecret\n")

169

assert result.exit_code == 0

170

assert "Login successful!" in result.output

171

172

def test_login_failure():

173

runner = CliRunner()

174

result = runner.invoke(app, input="user\nwrong\n")

175

assert result.exit_code == 1

176

assert "Login failed!" in result.output

177

178

if __name__ == "__main__":

179

test_login_success()

180

test_login_failure()

181

print("All tests passed!")

182

```

183

184

### Testing File Operations

185

186

```python

187

import typer

188

from typer.testing import CliRunner

189

from pathlib import Path

190

import tempfile

191

import os

192

193

app = typer.Typer()

194

195

@app.command()

196

def process_file(

197

input_file: Path = typer.Argument(..., exists=True),

198

output_file: Path = typer.Option("output.txt", "--output", "-o")

199

):

200

"""Process a file."""

201

content = input_file.read_text()

202

processed = content.upper()

203

output_file.write_text(processed)

204

typer.echo(f"Processed {input_file} -> {output_file}")

205

206

def test_process_file():

207

runner = CliRunner()

208

209

with tempfile.TemporaryDirectory() as temp_dir:

210

# Create test input file

211

input_path = Path(temp_dir) / "input.txt"

212

input_path.write_text("hello world")

213

214

output_path = Path(temp_dir) / "result.txt"

215

216

result = runner.invoke(app, [

217

str(input_path),

218

"--output", str(output_path)

219

])

220

221

assert result.exit_code == 0

222

assert "Processed" in result.output

223

assert output_path.read_text() == "HELLO WORLD"

224

225

def test_process_file_not_found():

226

runner = CliRunner()

227

result = runner.invoke(app, ["nonexistent.txt"])

228

assert result.exit_code != 0

229

230

if __name__ == "__main__":

231

test_process_file()

232

test_process_file_not_found()

233

print("All tests passed!")

234

```

235

236

### Testing Environment Variables

237

238

```python

239

import typer

240

from typer.testing import CliRunner

241

242

app = typer.Typer()

243

244

@app.command()

245

def connect(

246

host: str = typer.Option("localhost", envvar="DB_HOST"),

247

port: int = typer.Option(5432, envvar="DB_PORT"),

248

debug: bool = typer.Option(False, envvar="DEBUG")

249

):

250

"""Connect to database."""

251

typer.echo(f"Connecting to {host}:{port}")

252

if debug:

253

typer.echo("Debug mode enabled")

254

255

def test_connect_defaults():

256

runner = CliRunner()

257

result = runner.invoke(app, [])

258

assert result.exit_code == 0

259

assert "Connecting to localhost:5432" in result.output

260

assert "Debug mode" not in result.output

261

262

def test_connect_with_env():

263

runner = CliRunner()

264

result = runner.invoke(

265

app,

266

[],

267

env={"DB_HOST": "production", "DB_PORT": "3306", "DEBUG": "true"}

268

)

269

assert result.exit_code == 0

270

assert "Connecting to production:3306" in result.output

271

assert "Debug mode enabled" in result.output

272

273

def test_connect_cli_overrides_env():

274

runner = CliRunner()

275

result = runner.invoke(

276

app,

277

["--host", "staging", "--port", "5433"],

278

env={"DB_HOST": "production", "DB_PORT": "3306"}

279

)

280

assert result.exit_code == 0

281

assert "Connecting to staging:5433" in result.output

282

283

if __name__ == "__main__":

284

test_connect_defaults()

285

test_connect_with_env()

286

test_connect_cli_overrides_env()

287

print("All tests passed!")

288

```

289

290

### Testing Exception Handling

291

292

```python

293

import typer

294

from typer.testing import CliRunner

295

296

app = typer.Typer()

297

298

@app.command()

299

def divide(a: float, b: float):

300

"""Divide two numbers."""

301

if b == 0:

302

typer.echo("Error: Cannot divide by zero!", err=True)

303

raise typer.Exit(1)

304

305

result = a / b

306

typer.echo(f"{a} / {b} = {result}")

307

308

def test_divide_success():

309

runner = CliRunner()

310

result = runner.invoke(app, ["10", "2"])

311

assert result.exit_code == 0

312

assert "10.0 / 2.0 = 5.0" in result.output

313

314

def test_divide_by_zero():

315

runner = CliRunner()

316

result = runner.invoke(app, ["10", "0"])

317

assert result.exit_code == 1

318

assert "Cannot divide by zero!" in result.output

319

320

def test_divide_invalid_input():

321

runner = CliRunner()

322

result = runner.invoke(app, ["abc", "2"])

323

assert result.exit_code != 0

324

# Click will handle the type conversion error

325

326

if __name__ == "__main__":

327

test_divide_success()

328

test_divide_by_zero()

329

test_divide_invalid_input()

330

print("All tests passed!")

331

```

332

333

### Testing Sub-Applications

334

335

```python

336

import typer

337

from typer.testing import CliRunner

338

339

app = typer.Typer()

340

users_app = typer.Typer()

341

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

342

343

@users_app.command()

344

def create(name: str):

345

"""Create a user."""

346

typer.echo(f"Created user: {name}")

347

348

@users_app.command()

349

def delete(name: str):

350

"""Delete a user."""

351

typer.echo(f"Deleted user: {name}")

352

353

def test_users_create():

354

runner = CliRunner()

355

result = runner.invoke(app, ["users", "create", "alice"])

356

assert result.exit_code == 0

357

assert "Created user: alice" in result.output

358

359

def test_users_delete():

360

runner = CliRunner()

361

result = runner.invoke(app, ["users", "delete", "bob"])

362

assert result.exit_code == 0

363

assert "Deleted user: bob" in result.output

364

365

def test_users_help():

366

runner = CliRunner()

367

result = runner.invoke(app, ["users", "--help"])

368

assert result.exit_code == 0

369

assert "create" in result.output

370

assert "delete" in result.output

371

372

if __name__ == "__main__":

373

test_users_create()

374

test_users_delete()

375

test_users_help()

376

print("All tests passed!")

377

```

378

379

### Testing with Pytest

380

381

```python

382

import pytest

383

import typer

384

from typer.testing import CliRunner

385

386

app = typer.Typer()

387

388

@app.command()

389

def hello(name: str, count: int = 1):

390

"""Say hello."""

391

for _ in range(count):

392

typer.echo(f"Hello {name}!")

393

394

@pytest.fixture

395

def runner():

396

return CliRunner()

397

398

class TestHelloCommand:

399

def test_hello_basic(self, runner):

400

result = runner.invoke(app, ["World"])

401

assert result.exit_code == 0

402

assert "Hello World!" in result.output

403

404

def test_hello_with_count(self, runner):

405

result = runner.invoke(app, ["Alice", "--count", "3"])

406

assert result.exit_code == 0

407

assert result.output.count("Hello Alice!") == 3

408

409

@pytest.mark.parametrize("name,expected", [

410

("Bob", "Hello Bob!"),

411

("Charlie", "Hello Charlie!"),

412

("Diana", "Hello Diana!")

413

])

414

def test_hello_names(self, runner, name, expected):

415

result = runner.invoke(app, [name])

416

assert result.exit_code == 0

417

assert expected in result.output

418

```