0
# Testing Framework
1
2
Comprehensive testing utilities for both WSGI and ASGI Falcon applications. Provides request simulation, response validation, WebSocket testing, and mock objects for building robust test suites.
3
4
## Capabilities
5
6
### WSGI Test Client
7
8
Full-featured test client for simulating HTTP requests against WSGI applications.
9
10
```python { .api }
11
class TestClient:
12
def __init__(self, app: object, headers: dict = None):
13
"""
14
Create WSGI test client.
15
16
Args:
17
app: WSGI Falcon application instance
18
headers: Default headers for all requests
19
"""
20
21
def simulate_request(
22
self,
23
method: str,
24
path: str,
25
query_string: str = None,
26
headers: dict = None,
27
body: str = None,
28
json: object = None,
29
**kwargs
30
) -> Result:
31
"""
32
Simulate generic HTTP request.
33
34
Args:
35
method: HTTP method (GET, POST, PUT, etc.)
36
path: Request path (e.g., '/users/123')
37
query_string: URL query parameters
38
headers: Request headers
39
body: Raw request body
40
json: JSON request body (auto-serialized)
41
**kwargs: Additional WSGI environment variables
42
43
Returns:
44
Result object with response data
45
"""
46
47
def simulate_get(self, path: str, **kwargs) -> Result:
48
"""Simulate HTTP GET request"""
49
50
def simulate_post(self, path: str, **kwargs) -> Result:
51
"""Simulate HTTP POST request"""
52
53
def simulate_put(self, path: str, **kwargs) -> Result:
54
"""Simulate HTTP PUT request"""
55
56
def simulate_patch(self, path: str, **kwargs) -> Result:
57
"""Simulate HTTP PATCH request"""
58
59
def simulate_delete(self, path: str, **kwargs) -> Result:
60
"""Simulate HTTP DELETE request"""
61
62
def simulate_head(self, path: str, **kwargs) -> Result:
63
"""Simulate HTTP HEAD request"""
64
65
def simulate_options(self, path: str, **kwargs) -> Result:
66
"""Simulate HTTP OPTIONS request"""
67
```
68
69
#### Basic Testing Example
70
71
```python
72
import falcon
73
from falcon.testing import TestClient
74
75
class UserResource:
76
def on_get(self, req, resp, user_id=None):
77
if user_id:
78
resp.media = {'id': user_id, 'name': 'Test User'}
79
else:
80
resp.media = {'users': []}
81
82
def on_post(self, req, resp):
83
user_data = req.media
84
resp.status = falcon.HTTP_201
85
resp.media = {'created': user_data}
86
87
# Create app and test client
88
app = falcon.App()
89
app.add_route('/users', UserResource())
90
app.add_route('/users/{user_id}', UserResource())
91
92
client = TestClient(app)
93
94
# Test GET request
95
result = client.simulate_get('/users/123')
96
assert result.status_code == 200
97
assert result.json['name'] == 'Test User'
98
99
# Test POST request
100
result = client.simulate_post('/users', json={'name': 'New User'})
101
assert result.status_code == 201
102
assert result.json['created']['name'] == 'New User'
103
```
104
105
### ASGI Test Client
106
107
ASGI testing conductor for async applications and WebSocket testing.
108
109
```python { .api }
110
class ASGIConductor:
111
def __init__(self, app: object):
112
"""
113
Create ASGI test conductor.
114
115
Args:
116
app: ASGI Falcon application instance
117
"""
118
119
async def simulate_request(
120
self,
121
method: str,
122
path: str,
123
query_string: str = None,
124
headers: dict = None,
125
body: bytes = None,
126
json: object = None,
127
**kwargs
128
) -> Result:
129
"""
130
Simulate async HTTP request.
131
132
Args:
133
method: HTTP method
134
path: Request path
135
query_string: URL query parameters
136
headers: Request headers
137
body: Raw request body bytes
138
json: JSON request body (auto-serialized)
139
**kwargs: Additional ASGI scope variables
140
141
Returns:
142
Result object with response data
143
"""
144
145
async def simulate_get(self, path: str, **kwargs) -> Result:
146
"""Simulate async HTTP GET request"""
147
148
async def simulate_post(self, path: str, **kwargs) -> Result:
149
"""Simulate async HTTP POST request"""
150
151
async def simulate_put(self, path: str, **kwargs) -> Result:
152
"""Simulate async HTTP PUT request"""
153
154
async def simulate_patch(self, path: str, **kwargs) -> Result:
155
"""Simulate async HTTP PATCH request"""
156
157
async def simulate_delete(self, path: str, **kwargs) -> Result:
158
"""Simulate async HTTP DELETE request"""
159
160
async def simulate_head(self, path: str, **kwargs) -> Result:
161
"""Simulate async HTTP HEAD request"""
162
163
async def simulate_options(self, path: str, **kwargs) -> Result:
164
"""Simulate async HTTP OPTIONS request"""
165
```
166
167
### WebSocket Testing
168
169
Utilities for testing WebSocket connections in ASGI applications.
170
171
```python { .api }
172
class ASGIWebSocketSimulator:
173
def __init__(self, app: object, path: str, headers: dict = None):
174
"""
175
Create WebSocket test simulator.
176
177
Args:
178
app: ASGI Falcon application
179
path: WebSocket path
180
headers: Connection headers
181
"""
182
183
async def __aenter__(self):
184
"""Async context manager entry"""
185
return self
186
187
async def __aexit__(self, exc_type, exc_val, exc_tb):
188
"""Async context manager exit"""
189
190
async def send_text(self, text: str):
191
"""
192
Send text message to WebSocket.
193
194
Args:
195
text: Text message to send
196
"""
197
198
async def send_data(self, data: bytes):
199
"""
200
Send binary data to WebSocket.
201
202
Args:
203
data: Binary data to send
204
"""
205
206
async def receive_text(self) -> str:
207
"""
208
Receive text message from WebSocket.
209
210
Returns:
211
Text message received
212
"""
213
214
async def receive_data(self) -> bytes:
215
"""
216
Receive binary data from WebSocket.
217
218
Returns:
219
Binary data received
220
"""
221
222
async def close(self, code: int = 1000):
223
"""
224
Close WebSocket connection.
225
226
Args:
227
code: WebSocket close code
228
"""
229
```
230
231
#### ASGI and WebSocket Testing Example
232
233
```python
234
import falcon.asgi
235
from falcon.testing import ASGIConductor, ASGIWebSocketSimulator
236
237
class AsyncUserResource:
238
async def on_get(self, req, resp, user_id=None):
239
# Simulate async database call
240
user = await fetch_user(user_id)
241
resp.media = user
242
243
class WebSocketEcho:
244
async def on_websocket(self, req, ws):
245
await ws.accept()
246
while True:
247
try:
248
message = await ws.receive_text()
249
await ws.send_text(f"Echo: {message}")
250
except falcon.WebSocketDisconnected:
251
break
252
253
# Create ASGI app
254
app = falcon.asgi.App()
255
app.add_route('/users/{user_id}', AsyncUserResource())
256
app.add_route('/echo', WebSocketEcho())
257
258
# Test HTTP endpoint
259
conductor = ASGIConductor(app)
260
result = await conductor.simulate_get('/users/123')
261
assert result.status_code == 200
262
263
# Test WebSocket
264
async with ASGIWebSocketSimulator(app, '/echo') as ws:
265
await ws.send_text('Hello')
266
response = await ws.receive_text()
267
assert response == 'Echo: Hello'
268
```
269
270
### Test Result Objects
271
272
Objects representing HTTP response data for assertions and validation.
273
274
```python { .api }
275
class Result:
276
def __init__(self, status: str, headers: list, body: bytes):
277
"""
278
HTTP response result.
279
280
Args:
281
status: HTTP status line
282
headers: Response headers list
283
body: Response body bytes
284
"""
285
286
# Properties
287
status: str # Full status line (e.g., '200 OK')
288
status_code: int # Status code only (e.g., 200)
289
headers: dict # Headers as case-insensitive dict
290
text: str # Response body as text
291
content: bytes # Raw response body
292
json: object # Parsed JSON response (if applicable)
293
cookies: list # Response cookies
294
295
# Methods
296
def __len__(self) -> int:
297
"""Get response body length"""
298
299
def __iter__(self):
300
"""Iterate over response body bytes"""
301
302
class ResultBodyStream:
303
def __init__(self, result: Result):
304
"""
305
Streaming wrapper for large response bodies.
306
307
Args:
308
result: Result object to stream
309
"""
310
311
def read(self, size: int = -1) -> bytes:
312
"""Read bytes from response body"""
313
314
class StreamedResult:
315
def __init__(self, headers: dict, stream: object):
316
"""
317
Result for streamed responses.
318
319
Args:
320
headers: Response headers
321
stream: Response body stream
322
"""
323
324
# Properties
325
headers: dict
326
stream: object
327
328
class Cookie:
329
def __init__(self, name: str, value: str, **attributes):
330
"""
331
Response cookie representation.
332
333
Args:
334
name: Cookie name
335
value: Cookie value
336
**attributes: Cookie attributes (path, domain, secure, etc.)
337
"""
338
339
# Properties
340
name: str
341
value: str
342
path: str
343
domain: str
344
secure: bool
345
http_only: bool
346
max_age: int
347
expires: str
348
```
349
350
### Test Helper Functions
351
352
Utility functions for creating test environments and mock objects.
353
354
```python { .api }
355
# WSGI test helpers
356
def create_environ(
357
method: str = 'GET',
358
path: str = '/',
359
query_string: str = '',
360
headers: dict = None,
361
body: str = '',
362
**kwargs
363
) -> dict:
364
"""
365
Create WSGI environment dictionary for testing.
366
367
Args:
368
method: HTTP method
369
path: Request path
370
query_string: Query parameters
371
headers: Request headers
372
body: Request body
373
**kwargs: Additional environment variables
374
375
Returns:
376
WSGI environment dict
377
"""
378
379
def create_req(app: object, **kwargs) -> object:
380
"""
381
Create Request object for testing.
382
383
Args:
384
app: Falcon application
385
**kwargs: Environment parameters
386
387
Returns:
388
Request object
389
"""
390
391
# ASGI test helpers
392
def create_scope(
393
type: str = 'http',
394
method: str = 'GET',
395
path: str = '/',
396
query_string: str = '',
397
headers: list = None,
398
**kwargs
399
) -> dict:
400
"""
401
Create ASGI scope dictionary for testing.
402
403
Args:
404
type: ASGI scope type ('http' or 'websocket')
405
method: HTTP method
406
path: Request path
407
query_string: Query parameters
408
headers: Request headers as list of tuples
409
**kwargs: Additional scope variables
410
411
Returns:
412
ASGI scope dict
413
"""
414
415
def create_asgi_req(app: object, **kwargs) -> object:
416
"""
417
Create ASGI Request object for testing.
418
419
Args:
420
app: ASGI Falcon application
421
**kwargs: Scope parameters
422
423
Returns:
424
ASGI Request object
425
"""
426
427
# Event simulation
428
class ASGIRequestEventEmitter:
429
def __init__(self, body: bytes = b''):
430
"""
431
ASGI request event emitter for testing.
432
433
Args:
434
body: Request body bytes
435
"""
436
437
async def emit(self) -> dict:
438
"""Emit ASGI request events"""
439
440
class ASGIResponseEventCollector:
441
def __init__(self):
442
"""ASGI response event collector for testing."""
443
444
async def collect(self, message: dict):
445
"""Collect ASGI response events"""
446
447
def get_response(self) -> tuple:
448
"""Get collected response data"""
449
450
# Utility functions
451
def get_unused_port(family: int = socket.AF_INET, type: int = socket.SOCK_STREAM) -> int:
452
"""
453
Get unused network port for testing.
454
455
Args:
456
family: Socket family
457
type: Socket type
458
459
Returns:
460
Unused port number
461
"""
462
463
def rand_string(length: int, alphabet: str = None) -> str:
464
"""
465
Generate random string for testing.
466
467
Args:
468
length: String length
469
alphabet: Character set to use
470
471
Returns:
472
Random string
473
"""
474
```
475
476
### Test Resources and Mocks
477
478
Pre-built test resources and mock objects for common testing scenarios.
479
480
```python { .api }
481
class SimpleTestResource:
482
def __init__(self, status: str = '200 OK', body: str = 'Test'):
483
"""
484
Simple test resource for basic testing.
485
486
Args:
487
status: HTTP status to return
488
body: Response body
489
"""
490
491
def on_get(self, req, resp):
492
"""Handle GET requests"""
493
494
def on_post(self, req, resp):
495
"""Handle POST requests"""
496
497
class SimpleTestResourceAsync:
498
def __init__(self, status: str = '200 OK', body: str = 'Test'):
499
"""
500
Simple async test resource.
501
502
Args:
503
status: HTTP status to return
504
body: Response body
505
"""
506
507
async def on_get(self, req, resp):
508
"""Handle async GET requests"""
509
510
def capture_responder_args(*args, **kwargs) -> dict:
511
"""
512
Capture responder method arguments for testing.
513
514
Args:
515
*args: Positional arguments
516
**kwargs: Keyword arguments
517
518
Returns:
519
Captured arguments dict
520
"""
521
522
def set_resp_defaults(resp: object, base_resp: object):
523
"""
524
Set default response values for testing.
525
526
Args:
527
resp: Response object to configure
528
base_resp: Base response with defaults
529
"""
530
531
class StartResponseMock:
532
def __init__(self):
533
"""Mock WSGI start_response callable for testing."""
534
535
def __call__(self, status: str, headers: list, exc_info: tuple = None):
536
"""WSGI start_response interface"""
537
538
# Properties
539
status: str
540
headers: list
541
exc_info: tuple
542
```
543
544
### Test Case Base Class
545
546
Base test case class with Falcon-specific helper methods.
547
548
```python { .api }
549
class TestCase(unittest.TestCase):
550
def setUp(self):
551
"""Test setup with Falcon app creation."""
552
self.app = falcon.App()
553
self.client = TestClient(self.app)
554
555
def simulate_request(self, *args, **kwargs):
556
"""Simulate request using test client"""
557
return self.client.simulate_request(*args, **kwargs)
558
559
def simulate_get(self, *args, **kwargs):
560
"""Simulate GET request"""
561
return self.client.simulate_get(*args, **kwargs)
562
563
def simulate_post(self, *args, **kwargs):
564
"""Simulate POST request"""
565
return self.client.simulate_post(*args, **kwargs)
566
567
# Additional simulate_* methods for other HTTP verbs
568
```
569
570
#### Test Case Usage Example
571
572
```python
573
import unittest
574
import falcon
575
from falcon.testing import TestCase
576
577
class UserResourceTest(TestCase):
578
def setUp(self):
579
super().setUp()
580
self.app.add_route('/users/{user_id}', UserResource())
581
582
def test_get_user(self):
583
"""Test getting user by ID"""
584
result = self.simulate_get('/users/123')
585
self.assertEqual(result.status_code, 200)
586
self.assertEqual(result.json['id'], '123')
587
588
def test_user_not_found(self):
589
"""Test user not found scenario"""
590
result = self.simulate_get('/users/999')
591
self.assertEqual(result.status_code, 404)
592
593
def test_create_user(self):
594
"""Test user creation"""
595
user_data = {'name': 'Test User', 'email': 'test@example.com'}
596
result = self.simulate_post('/users', json=user_data)
597
self.assertEqual(result.status_code, 201)
598
self.assertEqual(result.json['name'], 'Test User')
599
```
600
601
## Types
602
603
```python { .api }
604
# Test clients
605
TestClient: type # WSGI test client
606
ASGIConductor: type # ASGI test conductor
607
608
# WebSocket testing
609
ASGIWebSocketSimulator: type
610
611
# Result objects
612
Result: type
613
ResultBodyStream: type
614
StreamedResult: type
615
Cookie: type
616
617
# Test helpers
618
create_environ: callable
619
create_req: callable
620
create_scope: callable
621
create_asgi_req: callable
622
623
# Event simulation
624
ASGIRequestEventEmitter: type
625
ASGIResponseEventCollector: type
626
627
# Utilities
628
get_unused_port: callable
629
rand_string: callable
630
631
# Test resources
632
SimpleTestResource: type
633
SimpleTestResourceAsync: type
634
capture_responder_args: callable
635
set_resp_defaults: callable
636
StartResponseMock: type
637
638
# Test case base
639
TestCase: type
640
```