0
# Testing Utilities
1
2
Testing support including base test classes for parser validation, common test scenarios, and utilities for testing request parsing across different web frameworks. The testing utilities provide a standardized approach to validating parser behavior and ensuring consistent functionality across framework implementations.
3
4
## Capabilities
5
6
### Common Test Case Base Class
7
8
Base test class that defines standard test methods for validating parser functionality across different web frameworks.
9
10
```python { .api }
11
class CommonTestCase:
12
"""
13
Base test class for validating parser functionality.
14
15
Provides common test methods that should work across all framework parsers.
16
Subclasses must implement create_app() to return a WSGI-compatible application.
17
18
Methods:
19
create_app(): Abstract method to create test application
20
create_testapp(app): Create webtest.TestApp wrapper
21
before_create_app(): Hook called before app creation
22
after_create_app(): Hook called after app creation
23
"""
24
25
def create_app(self):
26
"""
27
Create and return a WSGI-compatible test application.
28
29
Must be implemented by subclasses to create framework-specific test app
30
with routes for testing parser functionality.
31
32
Returns:
33
WSGI application: Test application with parser routes
34
35
Raises:
36
NotImplementedError: If not implemented by subclass
37
"""
38
raise NotImplementedError("Must define create_app()")
39
40
def create_testapp(self, app):
41
"""
42
Create webtest.TestApp wrapper for testing.
43
44
Args:
45
app: WSGI application to wrap
46
47
Returns:
48
webtest.TestApp: Test client for making requests
49
"""
50
return webtest.TestApp(app)
51
52
def before_create_app(self):
53
"""Hook called before application creation. Override for setup."""
54
pass
55
56
def after_create_app(self):
57
"""Hook called after application creation. Override for teardown."""
58
pass
59
```
60
61
### Test Fixtures
62
63
Pytest fixtures for setting up test applications and clients.
64
65
```python { .api }
66
@pytest.fixture(scope="class")
67
def testapp(self):
68
"""
69
Pytest fixture that provides a test application client.
70
71
Manages the lifecycle of test application creation and cleanup.
72
Calls before_create_app(), creates the app, wraps it in TestApp,
73
and calls after_create_app() for cleanup.
74
75
Yields:
76
webtest.TestApp: Test client for making HTTP requests
77
"""
78
```
79
80
### Standard Test Methods
81
82
Common test methods that validate core parsing functionality.
83
84
```python { .api }
85
def test_parse_querystring_args(self, testapp):
86
"""
87
Test parsing arguments from query string parameters.
88
89
Validates that query parameters are correctly parsed and validated
90
according to the defined schema.
91
92
Expected test route: GET /echo?name=Fred -> {"name": "Fred"}
93
"""
94
95
def test_parse_form(self, testapp):
96
"""
97
Test parsing arguments from form data.
98
99
Validates that form-encoded data is correctly parsed and validated.
100
101
Expected test route: POST /echo_form with form data -> parsed JSON
102
"""
103
104
def test_parse_json(self, testapp):
105
"""
106
Test parsing arguments from JSON request body.
107
108
Validates that JSON request bodies are correctly parsed and validated.
109
110
Expected test route: POST /echo_json with JSON body -> parsed JSON
111
"""
112
```
113
114
## Usage Examples
115
116
### Creating Framework-Specific Test Cases
117
118
```python
119
import pytest
120
from webargs.testing import CommonTestCase
121
from webargs import fields
122
from webargs.flaskparser import use_args
123
from flask import Flask, jsonify
124
125
class TestFlaskParser(CommonTestCase):
126
"""Test case for Flask parser using CommonTestCase."""
127
128
def create_app(self):
129
"""Create Flask test application with parser routes."""
130
app = Flask(__name__)
131
132
@app.route("/echo")
133
@use_args({"name": fields.Str()}, location="query")
134
def echo(args):
135
return jsonify(args)
136
137
@app.route("/echo_form", methods=["POST"])
138
@use_args({"name": fields.Str()}, location="form")
139
def echo_form(args):
140
return jsonify(args)
141
142
@app.route("/echo_json", methods=["POST"])
143
@use_args({"name": fields.Str()}, location="json")
144
def echo_json(args):
145
return jsonify(args)
146
147
return app
148
149
def test_custom_validation(self, testapp):
150
"""Additional test specific to Flask implementation."""
151
response = testapp.get("/echo?name=TestUser")
152
assert response.json == {"name": "TestUser"}
153
```
154
155
### Django Test Case Example
156
157
```python
158
from django.conf import settings
159
from django.test import RequestFactory
160
from webargs.testing import CommonTestCase
161
from webargs.djangoparser import use_args
162
from webargs import fields
163
164
class TestDjangoParser(CommonTestCase):
165
"""Test case for Django parser."""
166
167
def before_create_app(self):
168
"""Configure Django settings before app creation."""
169
if not settings.configured:
170
settings.configure(
171
SECRET_KEY='test-key',
172
ROOT_URLCONF='test_urls',
173
)
174
175
def create_app(self):
176
"""Create Django test application."""
177
from django.http import JsonResponse
178
from django.urls import path
179
180
@use_args({"name": fields.Str()}, location="query")
181
def echo_view(request, args):
182
return JsonResponse(args)
183
184
# Return WSGI application
185
from django.core.wsgi import get_wsgi_application
186
return get_wsgi_application()
187
```
188
189
### Async Framework Testing
190
191
```python
192
import asyncio
193
from webargs.testing import CommonTestCase
194
from webargs.aiohttpparser import use_args
195
from webargs import fields
196
from aiohttp import web
197
198
class TestAIOHTTPParser(CommonTestCase):
199
"""Test case for aiohttp parser with async support."""
200
201
def create_app(self):
202
"""Create aiohttp test application."""
203
app = web.Application()
204
205
@use_args({"name": fields.Str()}, location="query")
206
async def echo_handler(request, args):
207
return web.json_response(args)
208
209
app.router.add_get("/echo", echo_handler)
210
return app
211
212
def create_testapp(self, app):
213
"""Create async-compatible test client."""
214
import webtest_aiohttp
215
return webtest_aiohttp.TestApp(app)
216
```
217
218
### Custom Validation Testing
219
220
```python
221
class TestCustomValidation(CommonTestCase):
222
"""Test custom validation scenarios."""
223
224
def create_app(self):
225
app = Flask(__name__)
226
227
def validate_positive(value):
228
if value <= 0:
229
raise ValidationError("Must be positive")
230
return True
231
232
@app.route("/validate")
233
@use_args({
234
"number": fields.Int(validate=validate_positive)
235
}, location="query")
236
def validate_endpoint(args):
237
return jsonify(args)
238
239
return app
240
241
def test_positive_validation(self, testapp):
242
"""Test custom positive number validation."""
243
# Valid positive number
244
response = testapp.get("/validate?number=5")
245
assert response.json == {"number": 5}
246
247
# Invalid negative number should return 422
248
response = testapp.get("/validate?number=-1", expect_errors=True)
249
assert response.status_code == 422
250
```
251
252
### Error Handling Testing
253
254
```python
255
class TestErrorHandling(CommonTestCase):
256
"""Test error handling scenarios."""
257
258
def create_app(self):
259
app = Flask(__name__)
260
261
@app.route("/required")
262
@use_args({"name": fields.Str(required=True)}, location="query")
263
def required_endpoint(args):
264
return jsonify(args)
265
266
return app
267
268
def test_missing_required_field(self, testapp):
269
"""Test handling of missing required fields."""
270
response = testapp.get("/required", expect_errors=True)
271
assert response.status_code == 422
272
assert "required" in response.json.get("messages", {}).get("query", {}).get("name", [])
273
274
def test_validation_error_structure(self, testapp):
275
"""Test structure of validation error responses."""
276
response = testapp.get("/required", expect_errors=True)
277
278
# Verify error message structure
279
assert "messages" in response.json
280
assert "query" in response.json["messages"]
281
assert isinstance(response.json["messages"]["query"], dict)
282
```
283
284
### Multi-Location Parsing Tests
285
286
```python
287
class TestMultiLocationParsing(CommonTestCase):
288
"""Test parsing from multiple request locations."""
289
290
def create_app(self):
291
app = Flask(__name__)
292
293
@app.route("/multi", methods=["POST"])
294
@use_args({
295
"name": fields.Str(location="json"),
296
"page": fields.Int(location="query", missing=1),
297
"token": fields.Str(location="headers", data_key="Authorization")
298
})
299
def multi_location_endpoint(args):
300
return jsonify(args)
301
302
return app
303
304
def test_multi_location_parsing(self, testapp):
305
"""Test parsing from JSON, query, and headers simultaneously."""
306
response = testapp.post_json(
307
"/multi?page=2",
308
{"name": "test"},
309
headers={"Authorization": "Bearer token123"}
310
)
311
312
expected = {
313
"name": "test",
314
"page": 2,
315
"token": "Bearer token123"
316
}
317
assert response.json == expected
318
```
319
320
## Test Utilities
321
322
### Helper Functions
323
324
```python { .api }
325
def create_test_request(framework, method="GET", path="/", **kwargs):
326
"""
327
Create framework-specific test request object.
328
329
Args:
330
framework (str): Framework name ("flask", "django", etc.)
331
method (str): HTTP method
332
path (str): Request path
333
**kwargs: Additional request parameters
334
335
Returns:
336
Request object appropriate for the framework
337
"""
338
339
def assert_validation_error(response, field_name, location="json"):
340
"""
341
Assert that response contains validation error for specific field.
342
343
Args:
344
response: Test response object
345
field_name (str): Name of field with validation error
346
location (str): Request location where error occurred
347
"""
348
```
349
350
## Integration with Testing Frameworks
351
352
### Pytest Integration
353
354
```python
355
# conftest.py
356
import pytest
357
from webargs.testing import CommonTestCase
358
359
class BaseParserTest(CommonTestCase):
360
"""Base class for all parser tests."""
361
362
@pytest.fixture(autouse=True)
363
def setup_test_data(self):
364
"""Setup test data before each test."""
365
self.test_user = {"name": "Test User", "age": 25}
366
```
367
368
### Unittest Integration
369
370
```python
371
import unittest
372
from webargs.testing import CommonTestCase
373
374
class TestParser(CommonTestCase, unittest.TestCase):
375
"""Integration with unittest framework."""
376
377
def setUp(self):
378
"""Setup method called before each test."""
379
super().setUp()
380
self.test_data = {"key": "value"}
381
382
def tearDown(self):
383
"""Cleanup method called after each test."""
384
super().tearDown()
385
```
386
387
## Types
388
389
```python { .api }
390
TestApp = webtest.TestApp
391
WSGIApplication = typing.Callable[[dict, typing.Callable], typing.Iterable[bytes]]
392
```