TestClient para rutas HTML
El TestClient de FastAPI permite probar aplicaciones web de manera integral, incluyendo las rutas que renderizan templates HTML. A diferencia de las pruebas de API REST que verifican respuestas JSON, las pruebas de templates HTML requieren un enfoque diferente centrado en validar el contenido HTML renderizado.
Configuración básica de TestClient para templates
Para comenzar a probar rutas HTML, necesitamos configurar el TestClient de manera similar a como probamos endpoints de API, pero adaptando nuestras verificaciones para contenido HTML:
from fastapi import FastAPI, Request
from fastapi.templating import Jinja2Templates
from fastapi.testclient import TestClient
app = FastAPI()
templates = Jinja2Templates(directory="templates")
@app.get("/")
async def read_home(request: Request):
return templates.TemplateResponse(
"home.html",
{"request": request, "title": "Inicio"}
)
# Configuración del cliente de pruebas
client = TestClient(app)
La configuración es idéntica a las pruebas de API, pero las verificaciones cambian significativamente al trabajar con HTML.
Pruebas básicas de status code y content-type
Las primeras verificaciones en templates HTML son fundamentales para asegurar que la ruta responde correctamente:
def test_home_page_loads():
response = client.get("/")
# Verificar que la página carga correctamente
assert response.status_code == 200
# Verificar que devuelve HTML
assert "text/html" in response.headers["content-type"]
Estas verificaciones son el punto de partida para cualquier test de template, confirmando que la ruta está disponible y devuelve el tipo de contenido esperado.
Verificación de contenido HTML renderizado
Una vez confirmado que la ruta responde, podemos verificar el contenido HTML específico que genera el template:
def test_home_page_content():
response = client.get("/")
html_content = response.text
# Verificar título de la página
assert "<title>Inicio</title>" in html_content
# Verificar elementos específicos
assert "<h1>" in html_content
assert "Bienvenido" in html_content
El atributo response.text
contiene todo el HTML renderizado, permitiendo búsquedas de texto específicas dentro del contenido generado.
Pruebas de rutas con parámetros dinámicos
Las rutas que reciben parámetros y los pasan a los templates requieren verificaciones más específicas:
@app.get("/user/{user_id}")
async def user_profile(user_id: int, request: Request):
return templates.TemplateResponse(
"user.html",
{"request": request, "user_id": user_id, "name": f"Usuario {user_id}"}
)
def test_user_profile():
user_id = 123
response = client.get(f"/user/{user_id}")
assert response.status_code == 200
assert f"Usuario {user_id}" in response.text
assert f"ID: {user_id}" in response.text
Estas pruebas verifican que los datos dinámicos se rendericen correctamente en el template HTML.
Testing de formularios HTML con métodos GET y POST
Los formularios requieren probar tanto la renderización inicial como el procesamiento de datos:
@app.get("/contact")
async def contact_form(request: Request):
return templates.TemplateResponse(
"contact.html",
{"request": request}
)
@app.post("/contact")
async def submit_contact(request: Request):
form_data = await request.form()
return templates.TemplateResponse(
"contact_success.html",
{"request": request, "name": form_data.get("name")}
)
def test_contact_form_display():
response = client.get("/contact")
assert response.status_code == 200
assert '<form method="post"' in response.text
assert 'name="name"' in response.text
def test_contact_form_submission():
response = client.post(
"/contact",
data={"name": "Juan Pérez", "email": "juan@example.com"}
)
assert response.status_code == 200
assert "Juan Pérez" in response.text
assert "Mensaje enviado" in response.text
Manejo de errores en templates
Las pruebas también deben verificar el comportamiento de error cuando los templates no pueden renderizarse o los datos son inválidos:
@app.get("/product/{product_id}")
async def product_detail(product_id: int, request: Request):
# Simulamos búsqueda de producto
if product_id > 1000:
return templates.TemplateResponse(
"404.html",
{"request": request},
status_code=404
)
return templates.TemplateResponse(
"product.html",
{"request": request, "product_id": product_id}
)
def test_product_not_found():
response = client.get("/product/9999")
assert response.status_code == 404
assert "Producto no encontrado" in response.text
assert "text/html" in response.headers["content-type"]
Pruebas con dependencias y contexto
Cuando las rutas HTML utilizan dependencias de FastAPI, el TestClient las maneja automáticamente:
from fastapi import Depends
def get_current_user():
return {"id": 1, "name": "Test User"}
@app.get("/dashboard")
async def dashboard(request: Request, user=Depends(get_current_user)):
return templates.TemplateResponse(
"dashboard.html",
{"request": request, "user": user}
)
def test_dashboard_with_user():
response = client.get("/dashboard")
assert response.status_code == 200
assert "Test User" in response.text
assert "Dashboard" in response.text
El TestClient resuelve automáticamente las dependencias durante las pruebas, permitiendo probar rutas complejas sin configuración adicional.
Organización de pruebas para templates
Una estructura recomendada para organizar las pruebas de templates incluye separar las verificaciones por funcionalidad:
class TestHomePageTemplates:
def test_home_loads(self):
response = client.get("/")
assert response.status_code == 200
def test_home_contains_navigation(self):
response = client.get("/")
assert '<nav' in response.text
def test_home_has_footer(self):
response = client.get("/")
assert '<footer' in response.text
class TestUserTemplates:
def test_user_profile_loads(self):
response = client.get("/user/1")
assert response.status_code == 200
def test_user_profile_shows_data(self):
response = client.get("/user/1")
assert "Usuario 1" in response.text
Esta organización facilita el mantenimiento y la comprensión de las pruebas cuando el proyecto crece en complejidad.
Verificación de contenido templates
La verificación del contenido renderizado en templates va más allá de simples búsquedas de texto. Requiere técnicas específicas para validar la estructura HTML, datos dinámicos y elementos interactivos de manera robusta y mantenible.
Verificación de elementos HTML específicos
Una aproximación más precisa consiste en verificar elementos HTML específicos utilizando patrones de búsqueda más sofisticados que simples cadenas de texto:
import re
def test_product_card_structure():
response = client.get("/products/1")
html = response.text
# Verificar estructura de card de producto
assert '<div class="product-card">' in html
assert '<h2 class="product-title">' in html
assert '<span class="product-price">' in html
# Verificar que el precio tenga formato correcto
price_pattern = r'<span class="product-price">\$\d+\.\d{2}</span>'
assert re.search(price_pattern, html) is not None
Esta aproximación permite verificar no solo la presencia del contenido, sino también su estructura y formato específico dentro del HTML.
Validación de datos dinámicos en templates
Cuando los templates muestran datos dinámicos, las verificaciones deben adaptarse para validar que los datos se rendericen correctamente:
def test_user_list_displays_all_users():
# Simular datos de usuarios
test_users = [
{"id": 1, "name": "Ana García", "email": "ana@test.com"},
{"id": 2, "name": "Luis Martín", "email": "luis@test.com"}
]
response = client.get("/users")
html = response.text
# Verificar que cada usuario aparezca en el HTML
for user in test_users:
assert user["name"] in html
assert user["email"] in html
assert f'data-user-id="{user["id"]}"' in html
# Verificar que el contador de usuarios sea correcto
user_count_pattern = r'<span class="user-count">(\d+)</span>'
match = re.search(user_count_pattern, html)
assert match is not None
assert int(match.group(1)) == len(test_users)
Verificación de formularios y elementos interactivos
Los formularios requieren verificaciones específicas para asegurar que todos los campos y atributos estén presentes correctamente:
def test_registration_form_fields():
response = client.get("/register")
html = response.text
# Verificar campos obligatorios
required_fields = [
'name="username"',
'name="email"',
'name="password"',
'name="confirm_password"'
]
for field in required_fields:
assert field in html
# Verificar atributos de validación HTML5
assert 'type="email"' in html
assert 'required' in html
assert 'minlength="8"' in html
# Verificar token CSRF si está implementado
csrf_pattern = r'<input[^>]*name="csrf_token"[^>]*value="[^"]*"'
assert re.search(csrf_pattern, html) is not None
Verificación de contenido condicional
Los templates frecuentemente muestran contenido diferente basado en condiciones. Las pruebas deben verificar ambos escenarios:
def test_admin_panel_for_admin_user():
# Simular usuario administrador
response = client.get("/dashboard", headers={"X-User-Role": "admin"})
html = response.text
# Verificar elementos exclusivos de admin
assert '<button class="admin-delete">' in html
assert '<a href="/admin/settings">' in html
assert 'Gestión de usuarios' in html
def test_dashboard_for_regular_user():
# Simular usuario regular
response = client.get("/dashboard", headers={"X-User-Role": "user"})
html = response.text
# Verificar que NO aparezcan elementos de admin
assert '<button class="admin-delete">' not in html
assert '<a href="/admin/settings">' not in html
# Verificar elementos específicos de usuario regular
assert '<div class="user-profile">' in html
assert 'Mi perfil' in html
Verificación de estructura de listas y tablas
Cuando los templates renderizan listas o tablas de datos, las verificaciones deben validar tanto la estructura como el contenido:
def test_products_table_structure():
response = client.get("/products")
html = response.text
# Verificar estructura de tabla
assert '<table class="products-table">' in html
assert '<thead>' in html
assert '<tbody>' in html
# Verificar encabezados de columnas
expected_headers = ['Nombre', 'Precio', 'Stock', 'Acciones']
for header in expected_headers:
assert f'<th>{header}</th>' in html
# Contar filas de productos (excluyendo encabezado)
row_pattern = r'<tr[^>]*class="product-row"'
rows = re.findall(row_pattern, html)
assert len(rows) > 0 # Verificar que hay al menos un producto
# Verificar botones de acción en cada fila
edit_buttons = html.count('class="btn-edit"')
delete_buttons = html.count('class="btn-delete"')
assert edit_buttons == delete_buttons # Mismo número de botones
Verificación de enlaces y navegación
La navegación es un elemento crítico en las aplicaciones web que requiere verificación específica:
def test_navigation_menu():
response = client.get("/")
html = response.text
# Verificar enlaces de navegación principales
navigation_links = [
('<a href="/">', 'Inicio'),
('<a href="/products">', 'Productos'),
('<a href="/contact">', 'Contacto'),
('<a href="/about">', 'Acerca de')
]
for link_tag, link_text in navigation_links:
assert link_tag in html
assert link_text in html
# Verificar que los enlaces estén dentro del elemento nav
nav_pattern = r'<nav[^>]*>(.*?)</nav>'
nav_match = re.search(nav_pattern, html, re.DOTALL)
assert nav_match is not None
nav_content = nav_match.group(1)
for link_tag, _ in navigation_links:
assert link_tag in nav_content
Verificación de metadatos y SEO
Los templates también deben incluir metadatos apropiados que pueden verificarse en las pruebas:
def test_page_metadata():
response = client.get("/products/1")
html = response.text
# Verificar elementos SEO básicos
assert '<meta name="description"' in html
assert '<meta name="keywords"' in html
assert '<meta property="og:title"' in html
# Verificar título dinámico
title_pattern = r'<title>([^<]+)</title>'
title_match = re.search(title_pattern, html)
assert title_match is not None
assert "Producto" in title_match.group(1)
# Verificar meta viewport para responsive design
assert '<meta name="viewport" content="width=device-width, initial-scale=1.0">' in html
Verificación de assets y recursos externos
Las pruebas pueden verificar que los templates referencien correctamente los recursos externos:
def test_external_resources():
response = client.get("/")
html = response.text
# Verificar hojas de estilo CSS
css_pattern = r'<link[^>]*href="[^"]*\.css"'
css_links = re.findall(css_pattern, html)
assert len(css_links) > 0
# Verificar archivos JavaScript
js_pattern = r'<script[^>]*src="[^"]*\.js"'
js_scripts = re.findall(js_pattern, html)
assert len(js_scripts) > 0
# Verificar Bootstrap CDN si se utiliza
bootstrap_css = 'bootstrap.min.css'
bootstrap_js = 'bootstrap.bundle.min.js'
if bootstrap_css in html:
assert bootstrap_js in html # Si usa CSS debe usar JS también
Verificación de accesibilidad básica
Las pruebas pueden incluir verificaciones básicas de accesibilidad web:
def test_accessibility_features():
response = client.get("/contact")
html = response.text
# Verificar etiquetas alt en imágenes
img_without_alt = re.search(r'<img(?![^>]*alt=)', html)
assert img_without_alt is None, "Todas las imágenes deben tener atributo alt"
# Verificar labels para inputs
form_inputs = re.findall(r'<input[^>]*name="([^"]*)"', html)
for input_name in form_inputs:
label_pattern = f'<label[^>]*for="{input_name}"'
assert re.search(label_pattern, html), f"Input {input_name} debe tener label asociado"
# Verificar estructura de encabezados
h1_count = html.count('<h1')
assert h1_count == 1, "Debe haber exactamente un H1 por página"
Verificación con datos de prueba específicos
Para templates que muestran datos específicos, las verificaciones deben usar datos de prueba controlados:
def test_product_details_with_specific_data():
# Datos de prueba específicos
test_product = {
"id": 42,
"name": "Laptop Gaming Pro",
"price": 1299.99,
"stock": 5,
"description": "Laptop de alto rendimiento"
}
response = client.get(f"/products/{test_product['id']}")
html = response.text
# Verificar cada campo específico
assert test_product["name"] in html
assert f"${test_product['price']:.2f}" in html
assert f"Stock: {test_product['stock']}" in html
assert test_product["description"] in html
# Verificar que el ID aparezca en atributos de datos
assert f'data-product-id="{test_product["id"]}"' in html
# Verificar estado de stock
if test_product["stock"] > 0:
assert 'class="in-stock"' in html
assert 'Añadir al carrito' in html
else:
assert 'class="out-of-stock"' in html
assert 'Agotado' in html
Fuentes y referencias
Documentación oficial y recursos externos para profundizar en FastAPI
Documentación oficial de FastAPI
Alan Sastre
Ingeniero de Software y formador, CEO en CertiDevs
Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, FastAPI es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.
Más tutoriales de FastAPI
Explora más contenido relacionado con FastAPI y continúa aprendiendo con nuestros tutoriales gratuitos.
Aprendizajes de esta lección
- Configurar TestClient para probar rutas que devuelven templates HTML.
- Verificar status code, content-type y contenido HTML renderizado.
- Realizar pruebas con rutas que incluyen parámetros dinámicos y formularios.
- Validar estructura, datos dinámicos, formularios y elementos interactivos en templates.
- Organizar pruebas y manejar dependencias y errores en templates HTML.