or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

data-structures.mddev-server.mdexceptions.mdhttp-utilities.mdindex.mdmiddleware.mdrequest-response.mdrouting.mdsecurity.mdtesting.mdurl-wsgi-utils.md

testing.mddocs/

0

# Testing Utilities

1

2

Complete test client and utilities for testing WSGI applications including request simulation, cookie handling, redirect following, and response validation. These tools provide everything needed to thoroughly test web applications without running a server.

3

4

## Capabilities

5

6

### Test Client

7

8

The Client class provides a high-level interface for making HTTP requests to WSGI applications in test environments.

9

10

```python { .api }

11

class Client:

12

def __init__(self, application, response_wrapper=None, use_cookies=True, allow_subdomain_redirects=False):

13

"""

14

Create a test client for a WSGI application.

15

16

Parameters:

17

- application: WSGI application to test

18

- response_wrapper: Response class to wrap results (defaults to TestResponse)

19

- use_cookies: Whether to persist cookies between requests

20

- allow_subdomain_redirects: Allow following redirects to subdomains

21

"""

22

23

def open(self, *args, **kwargs):

24

"""

25

Make a request to the application.

26

27

Can be called with:

28

- open(path, method='GET', **kwargs)

29

- open(EnvironBuilder, **kwargs)

30

- open(Request, **kwargs)

31

32

Parameters:

33

- path: URL path to request

34

- method: HTTP method

35

- data: Request body data (string, bytes, dict, or file)

36

- json: JSON data to send (sets Content-Type automatically)

37

- headers: Request headers (dict or Headers object)

38

- query_string: URL parameters (string or dict)

39

- content_type: Content-Type header value

40

- auth: Authorization (username, password) tuple or Authorization object

41

- follow_redirects: Whether to follow HTTP redirects automatically

42

- buffered: Whether to buffer the response

43

- environ_base: Base WSGI environ values

44

- environ_overrides: WSGI environ overrides

45

46

Returns:

47

TestResponse object

48

"""

49

50

def get(self, *args, **kwargs):

51

"""Make a GET request. Same parameters as open() except method='GET'."""

52

53

def post(self, *args, **kwargs):

54

"""Make a POST request. Same parameters as open() except method='POST'."""

55

56

def put(self, *args, **kwargs):

57

"""Make a PUT request. Same parameters as open() except method='PUT'."""

58

59

def delete(self, *args, **kwargs):

60

"""Make a DELETE request. Same parameters as open() except method='DELETE'."""

61

62

def patch(self, *args, **kwargs):

63

"""Make a PATCH request. Same parameters as open() except method='PATCH'."""

64

65

def options(self, *args, **kwargs):

66

"""Make an OPTIONS request. Same parameters as open() except method='OPTIONS'."""

67

68

def head(self, *args, **kwargs):

69

"""Make a HEAD request. Same parameters as open() except method='HEAD'."""

70

71

def trace(self, *args, **kwargs):

72

"""Make a TRACE request. Same parameters as open() except method='TRACE'."""

73

74

# Cookie management

75

def get_cookie(self, key, domain="localhost", path="/"):

76

"""

77

Get a cookie by key, domain, and path.

78

79

Parameters:

80

- key: Cookie name

81

- domain: Cookie domain (default: 'localhost')

82

- path: Cookie path (default: '/')

83

84

Returns:

85

Cookie object or None if not found

86

"""

87

88

def set_cookie(self, key, value="", domain="localhost", origin_only=True, path="/", **kwargs):

89

"""

90

Set a cookie for subsequent requests.

91

92

Parameters:

93

- key: Cookie name

94

- value: Cookie value

95

- domain: Cookie domain

96

- origin_only: Whether domain must match exactly

97

- path: Cookie path

98

- **kwargs: Additional cookie parameters

99

"""

100

101

def delete_cookie(self, key, domain="localhost", path="/"):

102

"""

103

Delete a cookie.

104

105

Parameters:

106

- key: Cookie name

107

- domain: Cookie domain

108

- path: Cookie path

109

"""

110

```

111

112

### Environment Builder

113

114

EnvironBuilder creates WSGI environment dictionaries for testing, providing fine-grained control over request parameters.

115

116

```python { .api }

117

class EnvironBuilder:

118

def __init__(self, path="/", base_url=None, query_string=None, method="GET", input_stream=None, content_type=None, content_length=None, errors_stream=None, multithread=False, multiprocess=True, run_once=False, headers=None, data=None, environ_base=None, environ_overrides=None, mimetype=None, json=None, auth=None):

119

"""

120

Build a WSGI environment for testing.

121

122

Parameters:

123

- path: Request path (PATH_INFO in WSGI)

124

- base_url: Base URL for scheme, host, and script root

125

- query_string: URL parameters (string or dict)

126

- method: HTTP method (GET, POST, etc.)

127

- input_stream: Request body stream

128

- content_type: Content-Type header

129

- content_length: Content-Length header

130

- errors_stream: Error stream for wsgi.errors

131

- multithread: WSGI multithread flag

132

- multiprocess: WSGI multiprocess flag

133

- run_once: WSGI run_once flag

134

- headers: Request headers (list, dict, or Headers)

135

- data: Request body data (string, bytes, dict, or file)

136

- environ_base: Base environ values

137

- environ_overrides: Environ overrides

138

- mimetype: MIME type for data

139

- json: JSON data (sets Content-Type automatically)

140

- auth: Authorization (username, password) or Authorization object

141

"""

142

143

# Properties for accessing parsed data

144

form: MultiDict # Parsed form data

145

files: FileMultiDict # Uploaded files

146

args: MultiDict # Query string parameters

147

148

def get_environ(self):

149

"""

150

Build and return the WSGI environment dictionary.

151

152

Returns:

153

Complete WSGI environ dict

154

"""

155

156

def get_request(self, cls=None):

157

"""

158

Get a Request object from the environment.

159

160

Parameters:

161

- cls: Request class to use (defaults to werkzeug.wrappers.Request)

162

163

Returns:

164

Request object

165

"""

166

```

167

168

### Test Response

169

170

Enhanced response object with additional testing utilities and properties.

171

172

```python { .api }

173

class TestResponse(Response):

174

def __init__(self, response, status, headers, request, history, auto_to_bytes):

175

"""

176

Test-specific response wrapper.

177

178

Parameters:

179

- response: Response iterable

180

- status: HTTP status

181

- headers: Response headers

182

- request: Original request

183

- history: Redirect history

184

- auto_to_bytes: Whether to convert response to bytes

185

"""

186

187

# Additional properties for testing

188

request: Request # The original request

189

history: list[TestResponse] # Redirect history

190

text: str # Response body as text (decoded)

191

192

@property

193

def json(self):

194

"""

195

Parse response body as JSON.

196

197

Returns:

198

Parsed JSON data

199

200

Raises:

201

ValueError: If response is not valid JSON

202

"""

203

```

204

205

### Cookie

206

207

Represents a cookie with domain, path, and other attributes for testing.

208

209

```python { .api }

210

@dataclasses.dataclass

211

class Cookie:

212

key: str # Cookie name

213

value: str # Cookie value

214

domain: str # Cookie domain

215

path: str # Cookie path

216

origin_only: bool # Whether domain must match exactly

217

218

def should_send(self, server_name, path):

219

"""

220

Check if cookie should be sent with a request.

221

222

Parameters:

223

- server_name: Request server name

224

- path: Request path

225

226

Returns:

227

True if cookie should be included

228

"""

229

```

230

231

### Utility Functions

232

233

Helper functions for creating environments and running WSGI applications.

234

235

```python { .api }

236

def create_environ(*args, **kwargs):

237

"""

238

Create a WSGI environ dict. Shortcut for EnvironBuilder(...).get_environ().

239

240

Parameters:

241

Same as EnvironBuilder constructor

242

243

Returns:

244

WSGI environ dictionary

245

"""

246

247

def run_wsgi_app(app, environ, buffered=False):

248

"""

249

Run a WSGI application and capture the response.

250

251

Parameters:

252

- app: WSGI application callable

253

- environ: WSGI environment dictionary

254

- buffered: Whether to buffer the response

255

256

Returns:

257

Tuple of (app_iter, status, headers)

258

"""

259

260

def stream_encode_multipart(data, use_tempfile=True, threshold=1024*500, boundary=None):

261

"""

262

Encode form data as multipart/form-data stream.

263

264

Parameters:

265

- data: Dict of form fields and files

266

- use_tempfile: Use temp file for large data

267

- threshold: Size threshold for temp file

268

- boundary: Multipart boundary string

269

270

Returns:

271

Tuple of (stream, length, content_type)

272

"""

273

274

def encode_multipart(data, boundary=None, charset="utf-8"):

275

"""

276

Encode form data as multipart/form-data bytes.

277

278

Parameters:

279

- data: Dict of form fields and files

280

- boundary: Multipart boundary string

281

- charset: Character encoding

282

283

Returns:

284

Tuple of (data_bytes, content_type)

285

"""

286

```

287

288

### Exceptions

289

290

Testing-specific exceptions.

291

292

```python { .api }

293

class ClientRedirectError(Exception):

294

"""

295

Raised when redirect following fails or creates a loop.

296

"""

297

```

298

299

## Usage Examples

300

301

### Basic Application Testing

302

303

```python

304

from werkzeug.test import Client

305

from werkzeug.wrappers import Request, Response

306

307

# Simple WSGI application

308

def app(environ, start_response):

309

request = Request(environ)

310

311

if request.path == '/':

312

response = Response('Hello World!')

313

elif request.path == '/json':

314

response = Response('{"message": "Hello JSON"}', mimetype='application/json')

315

else:

316

response = Response('Not Found', status=404)

317

318

return response(environ, start_response)

319

320

# Test the application

321

def test_basic_requests():

322

client = Client(app)

323

324

# Test GET request

325

response = client.get('/')

326

assert response.status_code == 200

327

assert response.text == 'Hello World!'

328

329

# Test JSON endpoint

330

response = client.get('/json')

331

assert response.status_code == 200

332

assert response.json == {"message": "Hello JSON"}

333

334

# Test 404

335

response = client.get('/notfound')

336

assert response.status_code == 404

337

assert response.text == 'Not Found'

338

```

339

340

### Form Data and File Uploads

341

342

```python

343

from werkzeug.test import Client, EnvironBuilder

344

from werkzeug.datastructures import FileStorage

345

from io import BytesIO

346

347

def form_app(environ, start_response):

348

request = Request(environ)

349

350

if request.method == 'POST':

351

name = request.form.get('name', 'Anonymous')

352

email = request.form.get('email', '')

353

uploaded_file = request.files.get('avatar')

354

355

response_data = {

356

'name': name,

357

'email': email,

358

'file_uploaded': uploaded_file is not None,

359

'filename': uploaded_file.filename if uploaded_file else None

360

}

361

response = Response(str(response_data))

362

else:

363

response = Response('Send POST with form data')

364

365

return response(environ, start_response)

366

367

def test_form_submission():

368

client = Client(form_app)

369

370

# Test form data

371

response = client.post('/', data={

372

'name': 'John Doe',

373

'email': 'john@example.com'

374

})

375

assert 'John Doe' in response.text

376

assert 'john@example.com' in response.text

377

378

# Test file upload

379

response = client.post('/', data={

380

'name': 'Jane',

381

'avatar': (BytesIO(b'fake image data'), 'avatar.png')

382

})

383

assert 'file_uploaded": True' in response.text

384

assert 'avatar.png' in response.text

385

386

def test_with_environ_builder():

387

# More control with EnvironBuilder

388

builder = EnvironBuilder(

389

path='/upload',

390

method='POST',

391

data={

392

'description': 'Test file',

393

'file': FileStorage(

394

stream=BytesIO(b'test content'),

395

filename='test.txt',

396

content_type='text/plain'

397

)

398

}

399

)

400

401

environ = builder.get_environ()

402

request = Request(environ)

403

404

assert request.method == 'POST'

405

assert request.form['description'] == 'Test file'

406

assert request.files['file'].filename == 'test.txt'

407

```

408

409

### JSON API Testing

410

411

```python

412

import json

413

from werkzeug.test import Client

414

415

def api_app(environ, start_response):

416

request = Request(environ)

417

418

if request.path == '/api/data' and request.method == 'POST':

419

if request.is_json:

420

data = request.get_json()

421

response_data = {

422

'received': data,

423

'status': 'success'

424

}

425

response = Response(

426

json.dumps(response_data),

427

mimetype='application/json'

428

)

429

else:

430

response = Response(

431

'{"error": "Content-Type must be application/json"}',

432

status=400,

433

mimetype='application/json'

434

)

435

else:

436

response = Response('{"error": "Not found"}', status=404)

437

438

return response(environ, start_response)

439

440

def test_json_api():

441

client = Client(api_app)

442

443

# Test JSON request

444

test_data = {'name': 'Test', 'value': 123}

445

response = client.post(

446

'/api/data',

447

json=test_data # Automatically sets Content-Type

448

)

449

450

assert response.status_code == 200

451

assert response.json['status'] == 'success'

452

assert response.json['received'] == test_data

453

454

# Test non-JSON request

455

response = client.post(

456

'/api/data',

457

data='not json',

458

content_type='text/plain'

459

)

460

461

assert response.status_code == 400

462

assert 'Content-Type must be application/json' in response.json['error']

463

```

464

465

### Cookie Testing

466

467

```python

468

def cookie_app(environ, start_response):

469

request = Request(environ)

470

471

if request.path == '/set-cookie':

472

response = Response('Cookie set')

473

response.set_cookie('user_id', '12345', max_age=3600)

474

response.set_cookie('theme', 'dark', path='/settings')

475

elif request.path == '/check-cookie':

476

user_id = request.cookies.get('user_id')

477

theme = request.cookies.get('theme')

478

response = Response(f'User ID: {user_id}, Theme: {theme}')

479

else:

480

response = Response('Hello')

481

482

return response(environ, start_response)

483

484

def test_cookies():

485

client = Client(cookie_app, use_cookies=True)

486

487

# Set cookies

488

response = client.get('/set-cookie')

489

assert response.status_code == 200

490

491

# Cookies should be sent automatically

492

response = client.get('/check-cookie')

493

assert 'User ID: 12345' in response.text

494

495

# Manual cookie management

496

client.set_cookie('custom', 'value', domain='localhost')

497

cookie = client.get_cookie('custom', domain='localhost')

498

assert cookie.value == 'value'

499

500

# Check theme cookie with specific path

501

client.set_cookie('theme', 'light', path='/settings')

502

response = client.get('/settings/check-cookie')

503

# Theme cookie should be sent because path matches

504

```

505

506

### Authentication Testing

507

508

```python

509

from werkzeug.datastructures import Authorization

510

511

def auth_app(environ, start_response):

512

request = Request(environ)

513

514

if request.authorization:

515

if (request.authorization.username == 'admin' and

516

request.authorization.password == 'secret'):

517

response = Response(f'Welcome {request.authorization.username}!')

518

else:

519

response = Response('Invalid credentials', status=401)

520

else:

521

response = Response('Authentication required', status=401)

522

response.www_authenticate.set_basic('Test Realm')

523

524

return response(environ, start_response)

525

526

def test_authentication():

527

client = Client(auth_app)

528

529

# Test without auth

530

response = client.get('/protected')

531

assert response.status_code == 401

532

assert 'Authentication required' in response.text

533

534

# Test with basic auth (tuple shortcut)

535

response = client.get('/protected', auth=('admin', 'secret'))

536

assert response.status_code == 200

537

assert 'Welcome admin!' in response.text

538

539

# Test with Authorization object

540

auth = Authorization('basic', {'username': 'admin', 'password': 'secret'})

541

response = client.get('/protected', auth=auth)

542

assert response.status_code == 200

543

544

# Test invalid credentials

545

response = client.get('/protected', auth=('admin', 'wrong'))

546

assert response.status_code == 401

547

```

548

549

### Redirect Following

550

551

```python

552

def redirect_app(environ, start_response):

553

request = Request(environ)

554

555

if request.path == '/redirect':

556

response = Response('Redirecting...', status=302)

557

response.location = '/target'

558

elif request.path == '/target':

559

response = Response('You made it!')

560

else:

561

response = Response('Not found', status=404)

562

563

return response(environ, start_response)

564

565

def test_redirects():

566

client = Client(redirect_app)

567

568

# Don't follow redirects

569

response = client.get('/redirect')

570

assert response.status_code == 302

571

assert response.location == '/target'

572

573

# Follow redirects automatically

574

response = client.get('/redirect', follow_redirects=True)

575

assert response.status_code == 200

576

assert response.text == 'You made it!'

577

578

# Check redirect history

579

assert len(response.history) == 1

580

assert response.history[0].status_code == 302

581

```

582

583

### Custom Headers and Advanced Options

584

585

```python

586

def header_app(environ, start_response):

587

request = Request(environ)

588

589

user_agent = request.headers.get('User-Agent', 'Unknown')

590

custom_header = request.headers.get('X-Custom-Header', 'None')

591

592

response = Response(f'UA: {user_agent}, Custom: {custom_header}')

593

response.headers['X-Response-ID'] = '12345'

594

595

return response(environ, start_response)

596

597

def test_custom_headers():

598

client = Client(header_app)

599

600

response = client.get('/', headers={

601

'User-Agent': 'TestBot/1.0',

602

'X-Custom-Header': 'test-value',

603

'Accept': 'application/json'

604

})

605

606

assert 'UA: TestBot/1.0' in response.text

607

assert 'Custom: test-value' in response.text

608

assert response.headers['X-Response-ID'] == '12345'

609

610

def test_environ_customization():

611

# Custom WSGI environ values

612

response = client.get('/', environ_base={

613

'REMOTE_ADDR': '192.168.1.100',

614

'HTTP_HOST': 'testserver.com'

615

})

616

617

# Test with EnvironBuilder for more control

618

builder = EnvironBuilder(

619

path='/api/test',

620

method='PUT',

621

headers={'Authorization': 'Bearer token123'},

622

data='{"update": true}',

623

content_type='application/json'

624

)

625

626

response = client.open(builder)

627

```

628

629

### Error Handling and Edge Cases

630

631

```python

632

def test_error_handling():

633

client = Client(app)

634

635

# Test malformed requests

636

try:

637

# This should handle gracefully

638

response = client.post('/', data=b'\xff\xfe\invalid')

639

except Exception as e:

640

# Check specific error types

641

pass

642

643

# Test large uploads

644

large_data = b'x' * (1024 * 1024) # 1MB

645

response = client.post('/', data={'file': (BytesIO(large_data), 'big.txt')})

646

647

# Test cookies without cookie support

648

no_cookie_client = Client(app, use_cookies=False)

649

try:

650

no_cookie_client.get_cookie('test') # Should raise TypeError

651

except TypeError as e:

652

assert 'Cookies are disabled' in str(e)

653

```