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
```