Testing de Templates con TestClient

Intermedio
FastAPI
FastAPI
Actualizado: 18/09/2025

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 - Autor del tutorial

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.