0
# Testing Utilities
1
2
The testing utilities provide comprehensive tools for testing CLI commands and applications with simulated input/output, assertion helpers, and isolated test environments. These utilities enable thorough testing of command behavior without requiring real console interaction.
3
4
## Capabilities
5
6
### Command Testing
7
8
Test individual commands in isolation with controlled input and output.
9
10
```python { .api }
11
class CommandTester:
12
def __init__(self, command: Command) -> None:
13
"""
14
Create a tester for a specific command.
15
16
Args:
17
command (Command): Command instance to test
18
"""
19
20
def execute(self, args: str = "", inputs: str | None = None,
21
interactive: bool | None = None, verbosity: Verbosity | None = None,
22
decorated: bool | None = None) -> int:
23
"""
24
Execute the command with simulated input.
25
26
Args:
27
args (str): Command arguments and options as string
28
inputs (str | None): Simulated user input responses
29
interactive (bool | None): Override interactive mode detection
30
verbosity (Verbosity | None): Set specific verbosity level
31
decorated (bool | None): Override decoration detection
32
33
Returns:
34
int: Command exit code
35
"""
36
37
@property
38
def command(self) -> Command:
39
"""Get the command being tested."""
40
41
@property
42
def io(self) -> IO:
43
"""Get the IO interface used for testing."""
44
45
def get_display(self, normalize: bool = False) -> str:
46
"""
47
Get the output display content.
48
49
Args:
50
normalize (bool): Whether to normalize line endings
51
52
Returns:
53
str: Output content from the command execution
54
"""
55
56
def get_error_output(self, normalize: bool = False) -> str:
57
"""
58
Get the error output content.
59
60
Args:
61
normalize (bool): Whether to normalize line endings
62
63
Returns:
64
str: Error output content from the command execution
65
"""
66
67
def get_status_code(self) -> int:
68
"""
69
Get the last execution status code.
70
71
Returns:
72
int: Exit code from last execution
73
"""
74
```
75
76
### Application Testing
77
78
Test complete applications with multiple commands and routing.
79
80
```python { .api }
81
class ApplicationTester:
82
def __init__(self, application: Application) -> None:
83
"""
84
Create a tester for an application.
85
86
Args:
87
application (Application): Application instance to test
88
"""
89
90
def execute(self, args: str = "", inputs: str | None = None,
91
interactive: bool | None = None, verbosity: Verbosity | None = None,
92
decorated: bool | None = None) -> int:
93
"""
94
Execute application commands with simulated input.
95
96
Args:
97
args (str): Full command line including command name and arguments
98
inputs (str | None): Simulated user input responses
99
interactive (bool | None): Override interactive mode detection
100
verbosity (Verbosity | None): Set specific verbosity level
101
decorated (bool | None): Override decoration detection
102
103
Returns:
104
int: Application exit code
105
"""
106
107
@property
108
def application(self) -> Application:
109
"""Get the application being tested."""
110
111
@property
112
def io(self) -> IO:
113
"""Get the IO interface used for testing."""
114
115
def get_display(self, normalize: bool = False) -> str:
116
"""
117
Get the output display content.
118
119
Args:
120
normalize (bool): Whether to normalize line endings
121
122
Returns:
123
str: Output content from the application execution
124
"""
125
126
def get_error_output(self, normalize: bool = False) -> str:
127
"""
128
Get the error output content.
129
130
Args:
131
normalize (bool): Whether to normalize line endings
132
133
Returns:
134
str: Error output content from the application execution
135
"""
136
137
def get_status_code(self) -> int:
138
"""
139
Get the last execution status code.
140
141
Returns:
142
int: Exit code from last execution
143
"""
144
```
145
146
## Usage Examples
147
148
### Basic Command Testing
149
150
```python
151
import pytest
152
from cleo.testers.command_tester import CommandTester
153
from your_app.commands import GreetCommand
154
155
def test_greet_command_basic():
156
command = GreetCommand()
157
tester = CommandTester(command)
158
159
# Test with no arguments
160
exit_code = tester.execute()
161
assert exit_code == 0
162
assert "Hello" in tester.get_display()
163
164
def test_greet_command_with_name():
165
command = GreetCommand()
166
tester = CommandTester(command)
167
168
# Test with name argument
169
exit_code = tester.execute("John")
170
assert exit_code == 0
171
assert "Hello John" in tester.get_display()
172
173
def test_greet_command_with_yell_option():
174
command = GreetCommand()
175
tester = CommandTester(command)
176
177
# Test with --yell flag
178
exit_code = tester.execute("John --yell")
179
assert exit_code == 0
180
assert "HELLO JOHN" in tester.get_display()
181
```
182
183
### Testing Command Output and Formatting
184
185
```python
186
def test_status_command_output():
187
command = StatusCommand()
188
tester = CommandTester(command)
189
190
exit_code = tester.execute("--verbose")
191
output = tester.get_display()
192
193
# Test formatted output
194
assert exit_code == 0
195
assert "System Status:" in output
196
assert "Service 1: Running" in output
197
assert "Service 2: Stopped" in output
198
199
# Test verbose output only appears with flag
200
tester.execute("")
201
normal_output = tester.get_display()
202
assert len(output) > len(normal_output)
203
204
def test_error_output():
205
command = ProcessCommand()
206
tester = CommandTester(command)
207
208
# Test error conditions
209
exit_code = tester.execute("nonexistent-file.txt")
210
assert exit_code == 1
211
212
error_output = tester.get_error_output()
213
assert "File not found" in error_output
214
```
215
216
### Testing Interactive Commands
217
218
```python
219
def test_interactive_confirmation():
220
command = DeleteCommand()
221
tester = CommandTester(command)
222
223
# Test with "yes" input
224
exit_code = tester.execute("important-file.txt", inputs="yes\n")
225
assert exit_code == 0
226
assert "File deleted" in tester.get_display()
227
228
# Test with "no" input
229
exit_code = tester.execute("important-file.txt", inputs="no\n")
230
assert exit_code == 1
231
assert "Operation cancelled" in tester.get_display()
232
233
def test_interactive_questions():
234
command = ConfigCommand()
235
tester = CommandTester(command)
236
237
# Simulate multiple user inputs
238
inputs = "myproject\n8080\nyes\nproduction\n"
239
exit_code = tester.execute("", inputs=inputs)
240
241
assert exit_code == 0
242
output = tester.get_display()
243
assert "Project: myproject" in output
244
assert "Port: 8080" in output
245
assert "Environment: production" in output
246
247
def test_choice_questions():
248
command = SetupCommand()
249
tester = CommandTester(command)
250
251
# Test single choice
252
exit_code = tester.execute("", inputs="2\n") # Select second option
253
assert exit_code == 0
254
255
# Test invalid choice handling
256
exit_code = tester.execute("", inputs="invalid\n1\n") # Invalid then valid
257
assert exit_code == 0
258
assert "Invalid choice" in tester.get_display()
259
```
260
261
### Testing Command Arguments and Options
262
263
```python
264
def test_argument_validation():
265
command = ProcessCommand()
266
tester = CommandTester(command)
267
268
# Test missing required argument
269
exit_code = tester.execute("")
270
assert exit_code == 1
271
assert "Not enough arguments" in tester.get_error_output()
272
273
# Test valid arguments
274
exit_code = tester.execute("input.txt output.txt")
275
assert exit_code == 0
276
277
def test_option_parsing():
278
command = BuildCommand()
279
tester = CommandTester(command)
280
281
# Test short options
282
exit_code = tester.execute("-v -o dist/")
283
assert exit_code == 0
284
285
# Test long options
286
exit_code = tester.execute("--verbose --output-dir=dist/")
287
assert exit_code == 0
288
289
# Test option values
290
tester.execute("--format json")
291
assert command.option("format") == "json"
292
```
293
294
### Application-Level Testing
295
296
```python
297
def test_application_command_routing():
298
from your_app.application import create_application
299
300
app = create_application()
301
tester = ApplicationTester(app)
302
303
# Test help command
304
exit_code = tester.execute("help")
305
assert exit_code == 0
306
assert "Available commands:" in tester.get_display()
307
308
# Test list command
309
exit_code = tester.execute("list")
310
assert exit_code == 0
311
output = tester.get_display()
312
assert "process" in output # Our custom command should be listed
313
assert "config" in output
314
315
def test_application_command_execution():
316
app = create_application()
317
tester = ApplicationTester(app)
318
319
# Test executing specific commands
320
exit_code = tester.execute("process input.txt --format json")
321
assert exit_code == 0
322
323
exit_code = tester.execute("config --interactive", inputs="yes\nproduction\n")
324
assert exit_code == 0
325
326
def test_command_not_found():
327
app = create_application()
328
tester = ApplicationTester(app)
329
330
exit_code = tester.execute("nonexistent-command")
331
assert exit_code == 1
332
assert "Command not found" in tester.get_error_output()
333
```
334
335
### Testing Verbosity Levels
336
337
```python
338
def test_verbosity_levels():
339
command = DiagnosticCommand()
340
tester = CommandTester(command)
341
342
# Test quiet mode
343
exit_code = tester.execute("--quiet")
344
quiet_output = tester.get_display()
345
assert exit_code == 0
346
assert len(quiet_output.strip()) == 0 # No output in quiet mode
347
348
# Test normal verbosity
349
exit_code = tester.execute("")
350
normal_output = tester.get_display()
351
assert "Basic info" in normal_output
352
assert "Debug info" not in normal_output
353
354
# Test verbose mode
355
exit_code = tester.execute("--verbose")
356
verbose_output = tester.get_display()
357
assert "Basic info" in verbose_output
358
assert "Detailed info" in verbose_output
359
assert "Debug info" not in verbose_output
360
361
# Test debug mode
362
exit_code = tester.execute("-vvv")
363
debug_output = tester.get_display()
364
assert "Basic info" in debug_output
365
assert "Detailed info" in debug_output
366
assert "Debug info" in debug_output
367
```
368
369
### Testing Table Output
370
371
```python
372
def test_table_output():
373
command = ReportCommand()
374
tester = CommandTester(command)
375
376
exit_code = tester.execute("")
377
output = tester.get_display()
378
379
# Check table structure
380
assert "Name" in output # Header
381
assert "Status" in output # Header
382
assert "Service A" in output # Data row
383
assert "Running" in output # Data row
384
385
# Check table formatting (borders, alignment)
386
lines = output.split('\n')
387
header_line = next(line for line in lines if "Name" in line)
388
assert "│" in header_line or "|" in header_line # Table borders
389
390
def test_progress_output():
391
command = ProcessCommand()
392
tester = CommandTester(command)
393
394
exit_code = tester.execute("--show-progress")
395
output = tester.get_display()
396
397
# Progress bars create temporary output that gets cleared
398
# Test for completion message instead
399
assert "Processing complete" in output
400
assert exit_code == 0
401
```
402
403
### Test Fixtures and Helpers
404
405
```python
406
import pytest
407
import tempfile
408
import os
409
410
@pytest.fixture
411
def temp_dir():
412
"""Create a temporary directory for test files."""
413
with tempfile.TemporaryDirectory() as tmpdir:
414
yield tmpdir
415
416
@pytest.fixture
417
def sample_config_file(temp_dir):
418
"""Create a sample configuration file."""
419
config_path = os.path.join(temp_dir, "config.json")
420
with open(config_path, 'w') as f:
421
f.write('{"database": {"host": "localhost", "port": 5432}}')
422
return config_path
423
424
def test_command_with_files(sample_config_file):
425
command = LoadConfigCommand()
426
tester = CommandTester(command)
427
428
exit_code = tester.execute(f"--config {sample_config_file}")
429
assert exit_code == 0
430
assert "Configuration loaded" in tester.get_display()
431
432
class TestCommandHelper:
433
"""Helper class for common test patterns."""
434
435
@staticmethod
436
def execute_command(command_class, args="", inputs=None):
437
"""Execute a command and return tester for assertions."""
438
command = command_class()
439
tester = CommandTester(command)
440
exit_code = tester.execute(args, inputs)
441
return tester, exit_code
442
443
@staticmethod
444
def assert_success(tester, exit_code, expected_output=None):
445
"""Assert command executed successfully."""
446
assert exit_code == 0
447
if expected_output:
448
assert expected_output in tester.get_display()
449
450
@staticmethod
451
def assert_error(tester, exit_code, expected_error=None):
452
"""Assert command failed with error."""
453
assert exit_code != 0
454
if expected_error:
455
assert expected_error in tester.get_error_output()
456
457
# Usage of helper
458
def test_with_helper():
459
tester, exit_code = TestCommandHelper.execute_command(
460
GreetCommand,
461
"John --yell"
462
)
463
TestCommandHelper.assert_success(tester, exit_code, "HELLO JOHN")
464
```
465
466
### Parameterized Testing
467
468
```python
469
@pytest.mark.parametrize("name,expected", [
470
("John", "Hello John"),
471
("Alice", "Hello Alice"),
472
("", "Hello"),
473
])
474
def test_greet_variations(name, expected):
475
command = GreetCommand()
476
tester = CommandTester(command)
477
478
exit_code = tester.execute(name)
479
assert exit_code == 0
480
assert expected in tester.get_display()
481
482
@pytest.mark.parametrize("args,should_succeed", [
483
("valid-file.txt", True),
484
("", False), # Missing required argument
485
("nonexistent.txt", False), # File doesn't exist
486
])
487
def test_file_processing(args, should_succeed):
488
command = ProcessFileCommand()
489
tester = CommandTester(command)
490
491
exit_code = tester.execute(args)
492
493
if should_succeed:
494
assert exit_code == 0
495
assert "Success" in tester.get_display()
496
else:
497
assert exit_code != 0
498
```