Declarative parsing and validation of HTTP request objects, with built-in support for popular web frameworks.
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.
Base test class that defines standard test methods for validating parser functionality across different web frameworks.
class CommonTestCase:
"""
Base test class for validating parser functionality.
Provides common test methods that should work across all framework parsers.
Subclasses must implement create_app() to return a WSGI-compatible application.
Methods:
create_app(): Abstract method to create test application
create_testapp(app): Create webtest.TestApp wrapper
before_create_app(): Hook called before app creation
after_create_app(): Hook called after app creation
"""
def create_app(self):
"""
Create and return a WSGI-compatible test application.
Must be implemented by subclasses to create framework-specific test app
with routes for testing parser functionality.
Returns:
WSGI application: Test application with parser routes
Raises:
NotImplementedError: If not implemented by subclass
"""
raise NotImplementedError("Must define create_app()")
def create_testapp(self, app):
"""
Create webtest.TestApp wrapper for testing.
Args:
app: WSGI application to wrap
Returns:
webtest.TestApp: Test client for making requests
"""
return webtest.TestApp(app)
def before_create_app(self):
"""Hook called before application creation. Override for setup."""
pass
def after_create_app(self):
"""Hook called after application creation. Override for teardown."""
passPytest fixtures for setting up test applications and clients.
@pytest.fixture(scope="class")
def testapp(self):
"""
Pytest fixture that provides a test application client.
Manages the lifecycle of test application creation and cleanup.
Calls before_create_app(), creates the app, wraps it in TestApp,
and calls after_create_app() for cleanup.
Yields:
webtest.TestApp: Test client for making HTTP requests
"""Common test methods that validate core parsing functionality.
def test_parse_querystring_args(self, testapp):
"""
Test parsing arguments from query string parameters.
Validates that query parameters are correctly parsed and validated
according to the defined schema.
Expected test route: GET /echo?name=Fred -> {"name": "Fred"}
"""
def test_parse_form(self, testapp):
"""
Test parsing arguments from form data.
Validates that form-encoded data is correctly parsed and validated.
Expected test route: POST /echo_form with form data -> parsed JSON
"""
def test_parse_json(self, testapp):
"""
Test parsing arguments from JSON request body.
Validates that JSON request bodies are correctly parsed and validated.
Expected test route: POST /echo_json with JSON body -> parsed JSON
"""import pytest
from webargs.testing import CommonTestCase
from webargs import fields
from webargs.flaskparser import use_args
from flask import Flask, jsonify
class TestFlaskParser(CommonTestCase):
"""Test case for Flask parser using CommonTestCase."""
def create_app(self):
"""Create Flask test application with parser routes."""
app = Flask(__name__)
@app.route("/echo")
@use_args({"name": fields.Str()}, location="query")
def echo(args):
return jsonify(args)
@app.route("/echo_form", methods=["POST"])
@use_args({"name": fields.Str()}, location="form")
def echo_form(args):
return jsonify(args)
@app.route("/echo_json", methods=["POST"])
@use_args({"name": fields.Str()}, location="json")
def echo_json(args):
return jsonify(args)
return app
def test_custom_validation(self, testapp):
"""Additional test specific to Flask implementation."""
response = testapp.get("/echo?name=TestUser")
assert response.json == {"name": "TestUser"}from django.conf import settings
from django.test import RequestFactory
from webargs.testing import CommonTestCase
from webargs.djangoparser import use_args
from webargs import fields
class TestDjangoParser(CommonTestCase):
"""Test case for Django parser."""
def before_create_app(self):
"""Configure Django settings before app creation."""
if not settings.configured:
settings.configure(
SECRET_KEY='test-key',
ROOT_URLCONF='test_urls',
)
def create_app(self):
"""Create Django test application."""
from django.http import JsonResponse
from django.urls import path
@use_args({"name": fields.Str()}, location="query")
def echo_view(request, args):
return JsonResponse(args)
# Return WSGI application
from django.core.wsgi import get_wsgi_application
return get_wsgi_application()import asyncio
from webargs.testing import CommonTestCase
from webargs.aiohttpparser import use_args
from webargs import fields
from aiohttp import web
class TestAIOHTTPParser(CommonTestCase):
"""Test case for aiohttp parser with async support."""
def create_app(self):
"""Create aiohttp test application."""
app = web.Application()
@use_args({"name": fields.Str()}, location="query")
async def echo_handler(request, args):
return web.json_response(args)
app.router.add_get("/echo", echo_handler)
return app
def create_testapp(self, app):
"""Create async-compatible test client."""
import webtest_aiohttp
return webtest_aiohttp.TestApp(app)class TestCustomValidation(CommonTestCase):
"""Test custom validation scenarios."""
def create_app(self):
app = Flask(__name__)
def validate_positive(value):
if value <= 0:
raise ValidationError("Must be positive")
return True
@app.route("/validate")
@use_args({
"number": fields.Int(validate=validate_positive)
}, location="query")
def validate_endpoint(args):
return jsonify(args)
return app
def test_positive_validation(self, testapp):
"""Test custom positive number validation."""
# Valid positive number
response = testapp.get("/validate?number=5")
assert response.json == {"number": 5}
# Invalid negative number should return 422
response = testapp.get("/validate?number=-1", expect_errors=True)
assert response.status_code == 422class TestErrorHandling(CommonTestCase):
"""Test error handling scenarios."""
def create_app(self):
app = Flask(__name__)
@app.route("/required")
@use_args({"name": fields.Str(required=True)}, location="query")
def required_endpoint(args):
return jsonify(args)
return app
def test_missing_required_field(self, testapp):
"""Test handling of missing required fields."""
response = testapp.get("/required", expect_errors=True)
assert response.status_code == 422
assert "required" in response.json.get("messages", {}).get("query", {}).get("name", [])
def test_validation_error_structure(self, testapp):
"""Test structure of validation error responses."""
response = testapp.get("/required", expect_errors=True)
# Verify error message structure
assert "messages" in response.json
assert "query" in response.json["messages"]
assert isinstance(response.json["messages"]["query"], dict)class TestMultiLocationParsing(CommonTestCase):
"""Test parsing from multiple request locations."""
def create_app(self):
app = Flask(__name__)
@app.route("/multi", methods=["POST"])
@use_args({
"name": fields.Str(location="json"),
"page": fields.Int(location="query", missing=1),
"token": fields.Str(location="headers", data_key="Authorization")
})
def multi_location_endpoint(args):
return jsonify(args)
return app
def test_multi_location_parsing(self, testapp):
"""Test parsing from JSON, query, and headers simultaneously."""
response = testapp.post_json(
"/multi?page=2",
{"name": "test"},
headers={"Authorization": "Bearer token123"}
)
expected = {
"name": "test",
"page": 2,
"token": "Bearer token123"
}
assert response.json == expecteddef create_test_request(framework, method="GET", path="/", **kwargs):
"""
Create framework-specific test request object.
Args:
framework (str): Framework name ("flask", "django", etc.)
method (str): HTTP method
path (str): Request path
**kwargs: Additional request parameters
Returns:
Request object appropriate for the framework
"""
def assert_validation_error(response, field_name, location="json"):
"""
Assert that response contains validation error for specific field.
Args:
response: Test response object
field_name (str): Name of field with validation error
location (str): Request location where error occurred
"""# conftest.py
import pytest
from webargs.testing import CommonTestCase
class BaseParserTest(CommonTestCase):
"""Base class for all parser tests."""
@pytest.fixture(autouse=True)
def setup_test_data(self):
"""Setup test data before each test."""
self.test_user = {"name": "Test User", "age": 25}import unittest
from webargs.testing import CommonTestCase
class TestParser(CommonTestCase, unittest.TestCase):
"""Integration with unittest framework."""
def setUp(self):
"""Setup method called before each test."""
super().setUp()
self.test_data = {"key": "value"}
def tearDown(self):
"""Cleanup method called after each test."""
super().tearDown()TestApp = webtest.TestApp
WSGIApplication = typing.Callable[[dict, typing.Callable], typing.Iterable[bytes]]Install with Tessl CLI
npx tessl i tessl/pypi-webargs