or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

commands-groups.mdcontext-management.mdexception-handling.mdformatting.mdindex.mdparameter-types.mdparameters.mdterminal-ui.mdtesting-support.md

testing-support.mddocs/

0

# Testing Support

1

2

Built-in testing framework with CLI runner, result objects, and utilities for testing CLI applications in isolation with controlled input/output.

3

4

## Capabilities

5

6

### CLI Runner

7

8

Primary testing utility for invoking CLI commands in isolated environments.

9

10

```python { .api }

11

class CliRunner:

12

charset: str

13

env: Mapping[str, str]

14

echo_stdin: bool

15

mix_stderr: bool

16

17

def __init__(

18

self,

19

charset: str | None = None,

20

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

21

echo_stdin: bool = False,

22

mix_stderr: bool = True,

23

) -> None:

24

"""

25

CLI test runner for isolated command execution.

26

27

Parameters:

28

- charset: Character encoding for input/output

29

- env: Environment variables for test execution

30

- echo_stdin: Echo stdin to stdout for debugging

31

- mix_stderr: Mix stderr into stdout in results

32

33

Usage:

34

runner = click.testing.CliRunner()

35

result = runner.invoke(my_command, ['--help'])

36

"""

37

38

def invoke(

39

self,

40

cli: BaseCommand,

41

args: str | Iterable[str] | None = None,

42

input: bytes | str | IO[Any] | None = None,

43

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

44

catch_exceptions: bool = True,

45

color: bool = False,

46

**extra: Any,

47

) -> Result:

48

"""

49

Invoke a CLI command and return results.

50

51

Parameters:

52

- cli: Command or group to invoke

53

- args: Command arguments (list or string)

54

- input: Input data for stdin

55

- env: Environment variables (merged with runner env)

56

- catch_exceptions: Catch and store exceptions instead of raising

57

- color: Enable colored output

58

- **extra: Additional keyword arguments

59

60

Returns:

61

Result object with execution details

62

63

Usage:

64

# Basic command invocation

65

result = runner.invoke(hello_command)

66

67

# With arguments

68

result = runner.invoke(hello_command, ['--name', 'World'])

69

70

# With input

71

result = runner.invoke(interactive_command, input='y\\n')

72

73

# With environment

74

result = runner.invoke(env_command, env={'DEBUG': '1'})

75

"""

76

77

def isolated_filesystem(self) -> ContextManager[str]:

78

"""

79

Create isolated temporary filesystem for testing.

80

81

Returns:

82

Context manager yielding temporary directory path

83

84

Usage:

85

with runner.isolated_filesystem():

86

# Create test files

87

with open('test.txt', 'w') as f:

88

f.write('test content')

89

90

# Run command that operates on files

91

result = runner.invoke(process_command, ['test.txt'])

92

assert result.exit_code == 0

93

"""

94

95

def get_default_prog_name(self, cli: BaseCommand) -> str:

96

"""Get default program name for command."""

97

98

def make_env(self, overrides: Mapping[str, str] | None = None) -> dict[str, str]:

99

"""Create environment dict with overrides."""

100

```

101

102

### Test Result

103

104

Object containing the results of CLI command execution.

105

106

```python { .api }

107

class Result:

108

runner: CliRunner

109

exit_code: int

110

exception: Any

111

exc_info: Any | None

112

stdout_bytes: bytes

113

stderr_bytes: bytes

114

115

def __init__(

116

self,

117

runner: CliRunner,

118

stdout_bytes: bytes,

119

stderr_bytes: bytes,

120

exit_code: int,

121

exception: Any,

122

exc_info: Any | None = None,

123

) -> None:

124

"""

125

Result of CLI command execution.

126

127

Attributes:

128

- runner: CliRunner that produced this result

129

- exit_code: Command exit code

130

- exception: Exception that occurred (if any)

131

- exc_info: Exception info tuple

132

- stdout_bytes: Raw stdout bytes

133

- stderr_bytes: Raw stderr bytes

134

"""

135

136

@property

137

def output(self) -> str:

138

"""

139

Combined stdout output as string.

140

141

Returns:

142

Decoded stdout (and stderr if mixed)

143

"""

144

145

@property

146

def stdout(self) -> str:

147

"""

148

Stdout output as string.

149

150

Returns:

151

Decoded stdout

152

"""

153

154

@property

155

def stderr(self) -> str:

156

"""

157

Stderr output as string.

158

159

Returns:

160

Decoded stderr

161

"""

162

```

163

164

### Input Stream Utilities

165

166

Utilities for creating test input streams.

167

168

```python { .api }

169

def make_input_stream(input: bytes | str | IO[Any] | None, charset: str) -> BinaryIO:

170

"""

171

Create input stream for testing.

172

173

Parameters:

174

- input: Input data

175

- charset: Character encoding

176

177

Returns:

178

Binary input stream

179

180

Usage:

181

# Usually used internally by CliRunner

182

stream = make_input_stream('test input\\n', 'utf-8')

183

"""

184

185

class EchoingStdin:

186

"""

187

Stdin wrapper that echoes input to output for debugging.

188

189

Used internally when CliRunner.echo_stdin is True.

190

"""

191

192

def __init__(self, input: BinaryIO, output: BinaryIO) -> None: ...

193

def read(self, n: int = ...) -> bytes: ...

194

def readline(self, n: int = ...) -> bytes: ...

195

def readlines(self) -> list[bytes]: ...

196

```

197

198

### Testing Patterns

199

200

**Basic Command Testing:**

201

202

```python

203

import click

204

from click.testing import CliRunner

205

206

@click.command()

207

@click.option('--count', default=1, help='Number of greetings')

208

@click.argument('name')

209

def hello(count, name):

210

"""Simple greeting command."""

211

for _ in range(count):

212

click.echo(f'Hello {name}!')

213

214

def test_hello_command():

215

runner = CliRunner()

216

217

# Test basic functionality

218

result = runner.invoke(hello, ['World'])

219

assert result.exit_code == 0

220

assert 'Hello World!' in result.output

221

222

# Test with options

223

result = runner.invoke(hello, ['--count', '3', 'Alice'])

224

assert result.exit_code == 0

225

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

226

227

# Test help

228

result = runner.invoke(hello, ['--help'])

229

assert result.exit_code == 0

230

assert 'Simple greeting command' in result.output

231

```

232

233

**Testing Interactive Commands:**

234

235

```python

236

@click.command()

237

def interactive_setup():

238

"""Interactive setup command."""

239

name = click.prompt('Your name')

240

age = click.prompt('Your age', type=int)

241

if click.confirm('Save configuration?'):

242

click.echo(f'Saved config for {name}, age {age}')

243

else:

244

click.echo('Configuration not saved')

245

246

def test_interactive_setup():

247

runner = CliRunner()

248

249

# Test with confirmations

250

result = runner.invoke(interactive_setup, input='John\\n25\\ny\\n')

251

assert result.exit_code == 0

252

assert 'Saved config for John, age 25' in result.output

253

254

# Test with rejection

255

result = runner.invoke(interactive_setup, input='Jane\\n30\\nn\\n')

256

assert result.exit_code == 0

257

assert 'Configuration not saved' in result.output

258

```

259

260

**Testing File Operations:**

261

262

```python

263

@click.command()

264

@click.argument('filename', type=click.Path(exists=True))

265

def process_file(filename):

266

"""Process a file."""

267

with open(filename, 'r') as f:

268

content = f.read()

269

270

lines = len(content.splitlines())

271

click.echo(f'File has {lines} lines')

272

273

def test_process_file():

274

runner = CliRunner()

275

276

with runner.isolated_filesystem():

277

# Create test file

278

with open('test.txt', 'w') as f:

279

f.write('line 1\\nline 2\\nline 3\\n')

280

281

# Test command

282

result = runner.invoke(process_file, ['test.txt'])

283

assert result.exit_code == 0

284

assert 'File has 3 lines' in result.output

285

286

# Test missing file

287

result = runner.invoke(process_file, ['missing.txt'])

288

assert result.exit_code != 0

289

```

290

291

**Testing Environment Variables:**

292

293

```python

294

@click.command()

295

def env_command():

296

"""Command that uses environment variables."""

297

debug = os.environ.get('DEBUG', 'false').lower() == 'true'

298

if debug:

299

click.echo('Debug mode enabled')

300

else:

301

click.echo('Debug mode disabled')

302

303

def test_env_command():

304

runner = CliRunner()

305

306

# Test without environment

307

result = runner.invoke(env_command)

308

assert 'Debug mode disabled' in result.output

309

310

# Test with environment

311

result = runner.invoke(env_command, env={'DEBUG': 'true'})

312

assert 'Debug mode enabled' in result.output

313

```

314

315

**Testing Exception Handling:**

316

317

```python

318

@click.command()

319

@click.argument('number', type=int)

320

def divide_by_number(number):

321

"""Divide 100 by the given number."""

322

if number == 0:

323

raise click.BadParameter('Cannot divide by zero')

324

325

result = 100 / number

326

click.echo(f'100 / {number} = {result}')

327

328

def test_divide_by_number():

329

runner = CliRunner()

330

331

# Test normal operation

332

result = runner.invoke(divide_by_number, ['5'])

333

assert result.exit_code == 0

334

assert '100 / 5 = 20.0' in result.output

335

336

# Test error handling

337

result = runner.invoke(divide_by_number, ['0'])

338

assert result.exit_code != 0

339

assert 'Cannot divide by zero' in result.output

340

341

# Test with catch_exceptions=False to see actual exception

342

with pytest.raises(click.BadParameter):

343

runner.invoke(divide_by_number, ['0'], catch_exceptions=False)

344

```

345

346

**Testing Groups and Subcommands:**

347

348

```python

349

@click.group()

350

def cli():

351

"""Main CLI group."""

352

pass

353

354

@cli.command()

355

def status():

356

"""Show status."""

357

click.echo('Status: OK')

358

359

@cli.command()

360

@click.argument('name')

361

def greet(name):

362

"""Greet someone."""

363

click.echo(f'Hello {name}!')

364

365

def test_cli_group():

366

runner = CliRunner()

367

368

# Test group help

369

result = runner.invoke(cli, ['--help'])

370

assert result.exit_code == 0

371

assert 'Main CLI group' in result.output

372

373

# Test subcommand

374

result = runner.invoke(cli, ['status'])

375

assert result.exit_code == 0

376

assert 'Status: OK' in result.output

377

378

# Test subcommand with args

379

result = runner.invoke(cli, ['greet', 'World'])

380

assert result.exit_code == 0

381

assert 'Hello World!' in result.output

382

```

383

384

**Testing with Fixtures:**

385

386

```python

387

import pytest

388

389

@pytest.fixture

390

def runner():

391

"""CLI runner fixture."""

392

return CliRunner()

393

394

@pytest.fixture

395

def temp_config(runner):

396

"""Temporary config file fixture."""

397

with runner.isolated_filesystem():

398

config = {'host': 'localhost', 'port': 8080}

399

with open('config.json', 'w') as f:

400

json.dump(config, f)

401

yield 'config.json'

402

403

def test_with_fixtures(runner, temp_config):

404

result = runner.invoke(load_config_command, [temp_config])

405

assert result.exit_code == 0

406

```