Testing REROUTE Applications¶
Learn how to test your REROUTE routes, hooks, and decorators to ensure reliability and quality.
Overview¶
This guide focuses on testing REROUTE-specific features:
- Unit testing RouteBase classes
- Testing hooks (before_request, after_request, on_error)
- Testing REROUTE decorators (@rate_limit, @cache, @timeout)
- Testing route parameters
- Testing async route handlers
- Integration testing (HTTP endpoints)
- Database testing
- CI/CD integration
Quick Start¶
Install Testing Dependencies¶
Project Structure¶
my-app/
├── app/
│ ├── routes/
│ │ └── users/
│ │ └── page.py
│ ├── models/
│ └── database.py
├── tests/
│ ├── __init__.py
│ ├── conftest.py
│ ├── test_user_routes.py
│ └── test_hooks.py
└── pytest.ini
Unit Testing RouteBase Classes¶
Test your route classes directly without HTTP overhead.
Basic Route Class Test¶
# app/routes/users/page.py
from reroute import RouteBase
class UserRoutes(RouteBase):
def get(self):
"""Get all users."""
return {"users": self.get_all_users()}
def post(self, username: str, email: str):
"""Create a user."""
user = self.create_user(username, email)
return {"user": user, "created": True}
def get_all_users(self):
# Business logic
return [{"id": 1, "username": "alice"}]
def create_user(self, username, email):
# Business logic
return {"id": 2, "username": username, "email": email}
# tests/test_user_routes.py
import pytest
from app.routes.users.page import UserRoutes
def test_get_users():
"""Test GET handler returns user list."""
route = UserRoutes()
result = route.get()
assert "users" in result
assert isinstance(result["users"], list)
assert len(result["users"]) > 0
def test_post_user():
"""Test POST handler creates user."""
route = UserRoutes()
result = route.post(username="testuser", email="test@example.com")
assert result["created"] is True
assert result["user"]["username"] == "testuser"
assert result["user"]["email"] == "test@example.com"
def test_business_logic():
"""Test business logic methods directly."""
route = UserRoutes()
# Test get_all_users
users = route.get_all_users()
assert len(users) >= 1
# Test create_user
new_user = route.create_user("newuser", "new@example.com")
assert new_user["username"] == "newuser"
Testing Hooks¶
Testing before_request Hook¶
# app/routes/protected/page.py
from reroute import RouteBase
class ProtectedRoutes(RouteBase):
def before_request(self):
"""Check authentication before processing request."""
if not hasattr(self, 'auth_token'):
return {"error": "Missing auth token"}, 401
if self.auth_token != "valid-token-123":
return {"error": "Invalid token"}, 401
# Continue to handler
return None
def get(self):
return {"data": "protected data", "user": "authenticated"}
# tests/test_hooks.py
import pytest
from app.routes.protected.page import ProtectedRoutes
def test_before_request_missing_token():
"""Test before_request blocks request without token."""
route = ProtectedRoutes()
# Don't set auth_token
result = route.before_request()
assert result is not None # Hook returned early
response, status_code = result
assert status_code == 401
assert "error" in response
def test_before_request_invalid_token():
"""Test before_request blocks request with invalid token."""
route = ProtectedRoutes()
route.auth_token = "invalid-token"
result = route.before_request()
assert result is not None
response, status_code = result
assert status_code == 401
assert "Invalid token" in response["error"]
def test_before_request_valid_token():
"""Test before_request allows valid token."""
route = ProtectedRoutes()
route.auth_token = "valid-token-123"
result = route.before_request()
assert result is None # Hook allows request to continue
def test_get_with_valid_auth():
"""Test GET handler runs after successful before_request."""
route = ProtectedRoutes()
route.auth_token = "valid-token-123"
# before_request would pass
assert route.before_request() is None
# Now test handler
result = route.get()
assert result["data"] == "protected data"
Testing after_request Hook¶
# app/routes/api/page.py
from reroute import RouteBase
class APIRoutes(RouteBase):
def after_request(self, response):
"""Add metadata to all responses."""
if isinstance(response, dict):
response["meta"] = {
"version": "1.0",
"timestamp": "2025-11-23T10:00:00Z"
}
return response
def get(self):
return {"data": "some data"}
# tests/test_hooks.py
def test_after_request_adds_metadata():
"""Test after_request hook adds metadata."""
route = APIRoutes()
# Get original response
original = route.get()
assert "meta" not in original
# Apply after_request hook
result = route.after_request(original)
assert "meta" in result
assert result["meta"]["version"] == "1.0"
assert "timestamp" in result["meta"]
def test_after_request_preserves_data():
"""Test after_request preserves original data."""
route = APIRoutes()
original = {"data": "important data", "id": 123}
result = route.after_request(original)
assert result["data"] == "important data"
assert result["id"] == 123
Testing on_error Hook¶
# app/routes/users/page.py
from reroute import RouteBase
class UserRoutes(RouteBase):
def on_error(self, error: Exception):
"""Handle errors with appropriate responses."""
if isinstance(error, ValueError):
return {"error": "Invalid input", "message": str(error)}, 400
elif isinstance(error, KeyError):
return {"error": "Not found", "message": str(error)}, 404
else:
return {"error": "Internal error"}, 500
def get(self, user_id: int):
if user_id < 0:
raise ValueError("User ID must be positive")
if user_id == 99999:
raise KeyError("User not found")
return {"id": user_id, "username": "user"}
# tests/test_error_handling.py
import pytest
from app.routes.users.page import UserRoutes
def test_on_error_value_error():
"""Test on_error handles ValueError."""
route = UserRoutes()
error = ValueError("Invalid user ID")
response, status_code = route.on_error(error)
assert status_code == 400
assert response["error"] == "Invalid input"
assert "Invalid user ID" in response["message"]
def test_on_error_key_error():
"""Test on_error handles KeyError."""
route = UserRoutes()
error = KeyError("User not found")
response, status_code = route.on_error(error)
assert status_code == 404
assert response["error"] == "Not found"
def test_on_error_generic_exception():
"""Test on_error handles unknown exceptions."""
route = UserRoutes()
error = RuntimeError("Unexpected error")
response, status_code = route.on_error(error)
assert status_code == 500
assert response["error"] == "Internal error"
def test_handler_raises_value_error():
"""Test handler raises ValueError for invalid input."""
route = UserRoutes()
with pytest.raises(ValueError, match="must be positive"):
route.get(user_id=-1)
def test_handler_raises_key_error():
"""Test handler raises KeyError for not found."""
route = UserRoutes()
with pytest.raises(KeyError, match="not found"):
route.get(user_id=99999)
Testing REROUTE Decorators¶
Testing @rate_limit Decorator¶
# app/routes/api/page.py
from reroute import RouteBase
from reroute.decorators import rate_limit
class APIRoutes(RouteBase):
@rate_limit("3/min")
def get(self):
return {"data": "limited endpoint"}
# tests/test_decorators.py
import pytest
import time
from app.routes.api.page import APIRoutes
def test_rate_limit_allows_within_limit():
"""Test rate limiter allows requests within limit."""
route = APIRoutes()
# First 3 requests should succeed
for i in range(3):
result = route.get()
assert "data" in result
def test_rate_limit_blocks_over_limit():
"""Test rate limiter blocks requests over limit."""
route = APIRoutes()
# Exhaust rate limit
for i in range(3):
route.get()
# 4th request should be rate limited
result = route.get()
# Check if it's a tuple (error response with status code)
if isinstance(result, tuple):
response, status_code = result
assert status_code == 429
assert "rate limit" in response["error"].lower()
def test_rate_limit_resets():
"""Test rate limit resets after window."""
route = APIRoutes()
# Exhaust limit
for i in range(3):
route.get()
# Should be blocked
result = route.get()
if isinstance(result, tuple):
assert result[1] == 429
# Wait for rate limit window to reset
time.sleep(61) # Wait > 1 minute
# Should work again
result = route.get()
assert isinstance(result, dict)
assert "data" in result
Testing @cache Decorator¶
# app/routes/data/page.py
from reroute import RouteBase
from reroute.decorators import cache
class DataRoutes(RouteBase):
def __init__(self):
super().__init__()
self.call_count = 0
@cache(duration=60)
def get(self):
self.call_count += 1
return {"data": "expensive computation", "count": self.call_count}
# tests/test_cache.py
import pytest
from app.routes.data.page import DataRoutes
def test_cache_caches_result():
"""Test cache decorator caches results."""
route = DataRoutes()
# First call
result1 = route.get()
assert result1["count"] == 1
# Second call should be cached (count should still be 1)
result2 = route.get()
assert result2["count"] == 1 # Cached, no re-execution
def test_cache_expires():
"""Test cache expires after duration."""
import time
route = DataRoutes()
# First call
result1 = route.get()
count1 = result1["count"]
# Wait for cache to expire (use short duration in real tests)
# In actual test, use @cache(duration=1) for faster testing
time.sleep(2)
# Should execute again
result2 = route.get()
assert result2["count"] > count1
Testing @timeout Decorator¶
# app/routes/slow/page.py
from reroute import RouteBase
from reroute.decorators import timeout
import time
class SlowRoutes(RouteBase):
@timeout(seconds=2)
def get(self, delay: int = 0):
time.sleep(delay)
return {"completed": True}
# tests/test_timeout.py
import pytest
from app.routes.slow.page import SlowRoutes
def test_timeout_allows_fast_requests():
"""Test timeout allows requests that complete in time."""
route = SlowRoutes()
result = route.get(delay=1) # 1 second (under 2 second limit)
assert result["completed"] is True
def test_timeout_blocks_slow_requests():
"""Test timeout blocks requests that exceed limit."""
route = SlowRoutes()
result = route.get(delay=3) # 3 seconds (over 2 second limit)
# Should return timeout error
if isinstance(result, tuple):
response, status_code = result
assert status_code == 408
assert "timeout" in response["error"].lower()
Testing Async Route Handlers¶
# app/routes/async_users/page.py
from reroute import RouteBase
import asyncio
class AsyncUserRoutes(RouteBase):
async def get(self):
"""Async GET handler."""
await asyncio.sleep(0.1) # Simulate async operation
return {"users": await self.fetch_users()}
async def fetch_users(self):
await asyncio.sleep(0.05)
return [{"id": 1, "name": "Alice"}]
# tests/test_async_routes.py
import pytest
import asyncio
from app.routes.async_users.page import AsyncUserRoutes
@pytest.mark.asyncio
async def test_async_get_handler():
"""Test async GET handler."""
route = AsyncUserRoutes()
result = await route.get()
assert "users" in result
assert len(result["users"]) > 0
@pytest.mark.asyncio
async def test_async_fetch_users():
"""Test async fetch_users method."""
route = AsyncUserRoutes()
users = await route.fetch_users()
assert isinstance(users, list)
assert users[0]["name"] == "Alice"
Testing with Mock Data¶
Mocking External Dependencies¶
# app/routes/external/page.py
from reroute import RouteBase
import httpx
class ExternalAPIRoutes(RouteBase):
async def get(self):
data = await self.fetch_external_data()
return {"external_data": data}
async def fetch_external_data(self):
async with httpx.AsyncClient() as client:
response = await client.get("https://api.example.com/data")
return response.json()
# tests/test_external_api.py
import pytest
from unittest.mock import AsyncMock, patch
from app.routes.external.page import ExternalAPIRoutes
@pytest.mark.asyncio
async def test_fetch_external_data_success():
"""Test external API call with mocked response."""
route = ExternalAPIRoutes()
# Mock the httpx client
mock_response = AsyncMock()
mock_response.json.return_value = {"result": "mocked data"}
with patch('httpx.AsyncClient.get', return_value=mock_response):
result = await route.fetch_external_data()
assert result["result"] == "mocked data"
@pytest.mark.asyncio
async def test_get_with_mocked_data():
"""Test GET handler with mocked external call."""
route = ExternalAPIRoutes()
# Mock the fetch method
route.fetch_external_data = AsyncMock(return_value={"mocked": True})
result = await route.get()
assert result["external_data"]["mocked"] is True
Testing with Database¶
Using Test Database¶
# tests/conftest.py
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from database import Base
TEST_DB_URL = "sqlite:///:memory:"
@pytest.fixture(scope="function")
def db_session():
"""Create fresh database for each test."""
engine = create_engine(TEST_DB_URL)
Base.metadata.create_all(engine)
Session = sessionmaker(bind=engine)
session = Session()
yield session
session.close()
Base.metadata.drop_all(engine)
# app/routes/users/page.py
from reroute import RouteBase
from models import User
class UserRoutes(RouteBase):
def __init__(self, db_session=None):
super().__init__()
self.db = db_session
def get(self):
users = self.db.query(User).all()
return {"users": [u.to_dict() for u in users]}
def post(self, username: str, email: str):
user = User(username=username, email=email)
self.db.add(user)
self.db.commit()
return {"user": user.to_dict()}
# tests/test_database_routes.py
import pytest
from app.routes.users.page import UserRoutes
from models import User
def test_get_users_empty(db_session):
"""Test GET returns empty list initially."""
route = UserRoutes(db_session=db_session)
result = route.get()
assert result["users"] == []
def test_post_creates_user(db_session):
"""Test POST creates user in database."""
route = UserRoutes(db_session=db_session)
result = route.post(username="testuser", email="test@example.com")
# Verify in response
assert result["user"]["username"] == "testuser"
# Verify in database
user = db_session.query(User).filter_by(username="testuser").first()
assert user is not None
assert user.email == "test@example.com"
def test_get_users_returns_created(db_session):
"""Test GET returns created users."""
route = UserRoutes(db_session=db_session)
# Create users
route.post(username="user1", email="user1@example.com")
route.post(username="user2", email="user2@example.com")
# Get users
result = route.get()
assert len(result["users"]) == 2
assert result["users"][0]["username"] == "user1"
assert result["users"][1]["username"] == "user2"
Integration Testing (HTTP Endpoints)¶
For testing the full HTTP layer, use framework test clients:
FastAPI Integration Tests¶
# tests/test_integration.py
import pytest
from fastapi.testclient import TestClient
from main import app # Your REROUTE app
client = TestClient(app)
def test_get_users_endpoint():
"""Test GET /users HTTP endpoint."""
response = client.get("/users")
assert response.status_code == 200
assert "users" in response.json()
def test_post_user_endpoint():
"""Test POST /users HTTP endpoint."""
response = client.post("/users", json={
"username": "newuser",
"email": "new@example.com"
})
assert response.status_code == 201
assert response.json()["username"] == "newuser"
Flask Integration Tests¶
# tests/test_integration.py
import pytest
from main import app # Your REROUTE app
@pytest.fixture
def client():
app.config['TESTING'] = True
with app.test_client() as client:
yield client
def test_get_users_endpoint(client):
response = client.get('/users')
assert response.status_code == 200
assert 'users' in response.get_json()
Test Configuration¶
pytest.ini¶
[pytest]
testpaths = tests
python_files = test_*.py
python_functions = test_*
markers =
slow: slow tests
asyncio: async tests
integration: integration tests
addopts =
--verbose
--cov=app
--cov-report=term-missing
--cov-report=html
Running Tests¶
# Run all tests
pytest
# Run specific file
pytest tests/test_user_routes.py
# Run specific test
pytest tests/test_user_routes.py::test_get_users
# Run with coverage
pytest --cov=app --cov-report=html
# Run only unit tests (exclude integration)
pytest -m "not integration"
# Run async tests
pytest -m asyncio
# Verbose output
pytest -v
# Stop on first failure
pytest -x
Best Practices¶
1. Test RouteBase Classes Directly¶
# Good - Direct unit test
def test_business_logic():
route = UserRoutes()
result = route.create_user("test", "test@example.com")
assert result["username"] == "test"
# Avoid - Unnecessary HTTP overhead for unit test
def test_business_logic_http(client):
response = client.post("/users", json={"username": "test"})
assert response.status_code == 200
2. Test Hooks in Isolation¶
def test_before_request_logic():
"""Test before_request independently."""
route = ProtectedRoutes()
route.auth_token = "invalid"
result = route.before_request()
assert result[1] == 401 # Direct assertion on hook result
3. Use Fixtures for Route Instances¶
@pytest.fixture
def user_route(db_session):
"""Create UserRoutes instance with test database."""
return UserRoutes(db_session=db_session)
def test_with_fixture(user_route):
result = user_route.get()
assert "users" in result
4. Test One Thing Per Test¶
# Good
def test_get_returns_users():
route = UserRoutes()
result = route.get()
assert "users" in result
def test_get_returns_list():
route = UserRoutes()
result = route.get()
assert isinstance(result["users"], list)
# Avoid
def test_get_everything():
route = UserRoutes()
result = route.get()
assert "users" in result
assert isinstance(result["users"], list)
assert len(result["users"]) >= 0
# Too many assertions
CI/CD Integration¶
GitHub Actions¶
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install dependencies
run: |
pip install -e .
pip install pytest pytest-cov pytest-asyncio
- name: Run tests
run: pytest --cov=app --cov-report=xml
- name: Upload coverage
uses: codecov/codecov-action@v3
Next Steps¶
- Error Handling - Test error scenarios
- Security - Test authentication and authorization
- Lifecycle Hooks - Learn more about REROUTE hooks
- Decorators - Learn more about REROUTE decorators