¿Por qué este capítulo? Los proyectos reales tarde o temprano se topan con los problemas que no aparecen en una revisión manual rápida — una fuga de memoria que solo aparece después de semanas corriendo, una prueba inestable que erosiona la confianza en toda la suite, un bug que solo se reproduce bajo carga de producción. Este es el instrumental para esa etapa de la vida de un proyecto. ¿Qué trata de resolver Odoo con esto? Darle al equipo una forma de probar la integración entre servicios, conectar CI para detectar regresiones antes del despliegue, y depurar problemas de producción de forma segura — en vez de sesiones improvisadas de
console.logen el servidor en vivo de un cliente. Aplicación en la vida real: Diagnosticar una fuga de memoria que un cliente reporta después de un mes de uso diario, o configurar CI para que cada merge se pruebe automáticamente antes de llegar a su instancia.
En la Parte 1 cubrimos los fundamentos de pruebas y depuración de componentes OWL: escribir tus primeras pruebas, probar componentes que dependen de servicios, usar las DevTools del navegador efectivamente y seguir buenas prácticas de pruebas. Ahora iremos más lejos: patrones de pruebas avanzados, Desarrollo Dirigido por Pruebas, pruebas de rendimiento e integración, integración continua, errores en los que caen incluso desarrolladores experimentados, y cómo depurar problemas una vez que tu código está corriendo en producción.
Patrones Avanzados de Pruebas
1. Probando Comunicación de Componentes
Al probar la comunicación padre-hijo:
/** @odoo-module **/
import { Component, useState, xml } from "@odoo/owl";
import { describe, test, expect } from "@odoo/hoot";
import { click } from "@odoo/hoot-dom";
import { mountWithCleanup } from "@web/../tests/web_test_helpers";
import { Counter } from "../counter";
describe("Integración de Counter", () => {
test("comunicación padre-hijo", async () => {
const receivedEvents = [];
// Crear un componente padre que usa nuestro componente probado
class TestParent extends Component {
static template = xml`
<div>
<Counter initialValue="5" onValueChange.bind="onCounterChange"/>
<p class="parent-display">El padre ve: <t t-esc="state.lastValue"/></p>
</div>
`;
static components = { Counter };
setup() {
this.state = useState({ lastValue: 5 });
}
onCounterChange(newValue) {
receivedEvents.push(newValue);
this.state.lastValue = newValue;
}
}
await mountWithCleanup(TestParent);
// Interactuar con el componente hijo
await click(".increment-btn");
// Verificar que el padre recibió el evento
expect(receivedEvents).toEqual([6]);
expect(".parent-display").toHaveText("El padre ve: 6");
});
});
2. Probando con Cambios de Props
Probar cómo los componentes reaccionan a cambios de props. Como en OWL las props siempre fluyen hacia abajo desde un padre, la forma más limpia de probar esto es envolver el componente en un pequeño padre de prueba y cambiar lo que le pasa hacia abajo — el mismo patrón usado arriba:
describe("Integración de Counter", () => {
test("reacciona a cambios de props", async () => {
class TestParent extends Component {
static template = xml`<Counter max="state.max"/>`;
static components = { Counter };
setup() {
this.state = useState({ max: 5 });
}
}
await mountWithCleanup(TestParent);
// Incrementar hasta el máximo
for (let i = 0; i < 5; i++) {
await click(".increment-btn");
}
expect(".increment-btn").toHaveProperty("disabled", true);
// Subir el máximo en el estado del padre; OWL re-renderiza el hijo con las nuevas props
// Nota: este fragmento solo ilustra la idea — el estado de TestParent no es
// accesible desde afuera, así que en una prueba real expondrías una forma de
// actualizarlo (p. ej. un botón en la plantilla de TestParent) y harías clic ahí.
});
});
3. Probando Operaciones Asíncronas
Para componentes con operaciones asíncronas:
describe("Integración de TaskManager", () => {
test("maneja operaciones asíncronas", async () => {
let resolvePromise;
const asyncPromise = new Promise((resolve) => {
resolvePromise = resolve;
});
onRpc("project.task", "search_read", () => asyncPromise);
await mountWithCleanup(TaskManager);
// Verificar estado de carga
expect(".task-list p").toHaveCount(1);
// Resolver la operación asíncrona
resolvePromise([]);
await animationFrame(); // dejar que OWL procese el re-render tras resolver la promesa
// Verificar que la carga desapareció
expect(".task-list p").toHaveCount(0);
});
});
Agrega import { animationFrame } from "@odoo/hoot-dom"; al principio — es el helper genérico de "esperar un tick a que el DOM se actualice" en hoot, útil cuando resuelves una promesa a mano en vez de pasar por click() o edit() (que ya esperan internamente).
Desarrollo Dirigido por Pruebas (TDD) con OWL
El Desarrollo Dirigido por Pruebas es un enfoque poderoso donde escribes pruebas antes de implementar la funcionalidad:
1. Ciclo Rojo-Verde-Refactorizar
Rojo: Escribir una prueba que falle
test("el contador se muestra correctamente", async () => {
await mountWithCleanup(Counter);
expect(".counter-value").toHaveText("0");
});
Verde: Escribir código mínimo para que pase
export class Counter extends Component {
static template = xml`
<div>
<span class="counter-value">0</span>
</div>
`;
}
Refactorizar: Mejorar el código manteniendo las pruebas verdes
export class Counter extends Component {
static template = xml`
<div class="counter-widget">
<span class="counter-value" t-esc="state.count"/>
</div>
`;
setup() {
this.state = useState({ count: 0 });
}
}
2. Beneficios del TDD
- Requisitos Claros: Las pruebas sirven como especificaciones
- Mejor Diseño: Escribir pruebas primero lleva a código más testeable y modular
- Protección contra Regresiones: Suite integral de pruebas detecta rupturas futuras
- Confianza: Pruebas verdes significan software funcionando
Pruebas de Rendimiento y Depuración
1. Midiendo el Rendimiento de Componentes
test("rendimiento del contador con muchas actualizaciones", async () => {
await mountWithCleanup(Counter, { props: { max: 1000 } });
const startTime = performance.now();
// Simular clics rápidos
for (let i = 0; i < 100; i++) {
await click(".increment-btn");
}
const duration = performance.now() - startTime;
expect(duration).toBeLessThan(1000);
expect(".counter-value").toHaveText("100");
});
2. Detección de Fugas de Memoria
Esta prueba gestiona el montaje/destrucción a mano en vez de usar mountWithCleanup(), ya que su propósito es verificar que la limpieza manual no deja nada atrás — así que importa mount directamente de @odoo/owl:
import { mount } from "@odoo/owl";
test("el contador se limpia apropiadamente", async () => {
const initialComponents = document.querySelectorAll(".counter-widget").length;
const container = document.createElement("div");
// Crear y destruir múltiples componentes
for (let i = 0; i < 10; i++) {
const counter = await mount(Counter, container);
counter.destroy();
container.innerHTML = ""; // Limpiar el contenedor
}
// Forzar recolección de basura si está disponible
if (window.gc) {
window.gc();
}
const finalComponents = document.querySelectorAll(".counter-widget").length;
expect(finalComponents).toBe(initialComponents);
});
Pruebas de Integración
Probando Componentes en Escenarios Reales
A veces necesitas probar cómo los componentes trabajan juntos en condiciones realistas:
test("el contador funciona en contexto de formulario", async () => {
// Crear un componente padre realista
class TestForm extends Component {
static template = xml`
<form t-on-submit.prevent="onSubmit">
<div class="form-group">
<label>Cantidad:</label>
<Counter initialValue="1"
min="1"
max="10"
onValueChange.bind="onQuantityChange"/>
</div>
<div class="form-group">
<span>Total: $<t t-esc="state.total"/></span>
</div>
<button type="submit" class="btn btn-primary">Ordenar</button>
</form>
`;
static components = { Counter };
setup() {
this.state = useState({
quantity: 1,
price: 10,
total: 10,
});
}
onQuantityChange(newQuantity) {
this.state.quantity = newQuantity;
this.state.total = this.state.quantity * this.state.price;
}
onSubmit() {
// Lógica de envío del formulario
}
}
await mountWithCleanup(TestForm);
// Probar comportamiento integrado
await click(".increment-btn");
expect(".counter-value").toHaveText("2");
expect(".form-group:nth-child(2)").toHaveText("Total: $20");
});
Integración Continua y Pruebas
1. Ejecutando Pruebas en CI/CD
Crear un script de prueba para tu módulo:
Archivo: mi_modulo/tests/test_js.py
import odoo.tests
@odoo.tests.tagged('post_install', '-at_install')
class TestJavaScript(odoo.tests.HttpCase):
def test_js_modules(self):
"""Probar que los módulos JavaScript cargan y las pruebas unitarias de hoot pasan"""
self.browser_js(
"/web/tests?module=mi_modulo&failfast",
code="",
timeout=60,
)
2. Reportes de Cobertura de Pruebas
La cobertura para las pruebas de hoot se habilita desde el propio runner de pruebas (p. ej. un parámetro de URL coverage=1 en /web/tests), no desde dentro del archivo de prueba. Una vez que termina una ejecución con cobertura habilitada, el objeto de cobertura estándar de Istanbul queda disponible en la página para inspeccionarlo o exportarlo:
if (window.__coverage__) {
console.log("Datos de cobertura:", window.__coverage__);
}
Errores Comunes de Pruebas y Soluciones
1. Pruebas Inestables
Problema: Pruebas que a veces pasan y a veces fallan
Solución: Asegurar el manejo apropiado de async
// Malo - no esperar operaciones asíncronas
test("prueba inestable", async () => {
await mountWithCleanup(Counter);
click(".increment-btn"); // ¡No se esperó!
expect(".counter-value").toHaveText("1");
});
// Bueno - esperar apropiadamente operaciones asíncronas
test("prueba confiable", async () => {
await mountWithCleanup(Counter);
await click(".increment-btn"); // click() de hoot ya espera el re-renderizado
expect(".counter-value").toHaveText("1");
});
2. Probando Métodos Privados
Problema: Querer probar métodos internos del componente
Solución: Probar a través de la interfaz pública
// Malo - probando métodos privados
test("_calculateTotal funciona", async () => {
const component = await mountWithCleanup(MyComponent);
const result = component._calculateTotal(5, 10);
expect(result).toBe(50);
});
// Bueno - probando a través del comportamiento público
test("muestra el total correcto cuando cambia la cantidad", async () => {
await mountWithCleanup(MyComponent);
await edit(".quantity-input", "5");
expect(".total").toHaveText("Total: 50");
});
3. Exceso de Mocking
Problema: Hacer mock de demasiadas dependencias
Solución: Hacer mock solo de lo necesario
// Malo - exceso de mocking de cada servicio que toca el componente
mockService("orm", mockOrm);
mockService("notification", mockNotification);
mockService("user", mockUser);
mockService("company", mockCompany);
// ... hacer mock de todo vuelve la prueba frágil y esconde bugs reales de integración
// Bueno - hacer mock solo de la llamada RPC específica que le importa a esta prueba
onRpc("mi.modelo", "mi_metodo_especifico", () => mockData);
// el resto de las solicitudes pasan por el manejo normal
Depurando Problemas de Producción
1. Límites de Error y Logging
export class ErrorBoundary extends Component {
static template = xml`
<div>
<t t-if="state.hasError">
<div class="alert alert-danger">
<h4>Algo salió mal</h4>
<p t-esc="state.error.message"/>
<button t-on-click="retry">Intentar de nuevo</button>
</div>
</t>
<t t-else="">
<t t-slot="default"/>
</t>
</div>
`;
setup() {
this.state = useState({
hasError: false,
error: null
});
}
catchError(error) {
console.error("Error de componente capturado:", error);
// Enviar al servicio de reporte de errores
if (window.errorReporting) {
window.errorReporting.captureException(error);
}
this.state.hasError = true;
this.state.error = error;
}
retry() {
this.state.hasError = false;
this.state.error = null;
}
}
2. Feature Flags para Despliegues Seguros
export class Counter extends Component {
setup() {
this.featureFlags = useService("feature_flags");
this.state = useState({
count: this.props.initialValue || 0
});
}
increment() {
if (this.featureFlags.isEnabled("advanced_counter")) {
// Nueva implementación
this.state.count += this.props.step || 1;
} else {
// Respaldo seguro
this.state.count++;
}
}
}
Ejercicios
Ejercicio 1: Escribe un Ciclo de TDD
Siguiendo el ciclo Rojo-Verde-Refactorizar, escribe una prueba que falle para un nuevo getter doubled en Counter que devuelva state.count * 2, implementa el código mínimo para que pase, y luego refactoriza si ves margen de mejora.
Ejercicio 2: Hacer Mock de un Servicio y Verificar sus Argumentos
Escribe una prueba para TaskManager.deleteTask que haga mock del servicio orm directamente con mockService("orm", ...) (en vez de pasar por onRpc) y verifique que orm.unlink fue llamado con ["project.task", [taskId]].
Ejercicio 3: Esboza una Verificación de CI
Esboza (en comentarios, sin necesidad de ejecutarlo) qué agregarías a test_js.py para que CI falle el build si alguna prueba de hoot de tu módulo falla. En 2-3 frases, explica por qué ejecutar pruebas JS en CI importa aunque ya pasen en tu máquina.
Resumen: Construyendo Aplicaciones OWL Robustas
Las pruebas y la depuración no son ideas tardías: son partes integrales de construir aplicaciones OWL profesionales. Aquí está tu hoja de ruta hacia la excelencia:
1. Estrategia de Pruebas
- Pruebas Unitarias: Probar componentes individuales de forma aislada
- Pruebas de Integración: Probar interacciones entre componentes
- Pruebas de Extremo a Extremo: Probar flujos de trabajo completos del usuario
- Pruebas de Rendimiento: Asegurar tiempos de respuesta aceptables
- Pruebas de Manejo de Errores: Verificar modos de falla elegantes
2. Kit de Herramientas de Depuración
- DevTools del Navegador: Tu interfaz principal de depuración
- Logging Estratégico: Capturar cambios importantes de estado
- Límites de Error: Manejo elegante de errores
- Monitoreo de Rendimiento: Rastrear y optimizar operaciones lentas
3. Prácticas Profesionales
- Desarrollo Dirigido por Pruebas: Escribir pruebas antes de la implementación
- Integración Continua: Ejecución automatizada de pruebas
- Cobertura de Código: Asegurar cobertura adecuada de pruebas
- Documentación: Especificaciones claras y ejecutables
4. Mentalidad de Mantenimiento
- Refactorizar con Confianza: Las buenas pruebas permiten cambios seguros
- Prevención de Regresiones: Las pruebas detectan rupturas inesperadas
- Documentación Viviente: Las pruebas muestran cómo deberían funcionar los componentes
- Colaboración en Equipo: Entendimiento compartido a través de pruebas
Al dominar estas técnicas de pruebas y depuración, te transformas de alguien que escribe código que funciona hoy en alguien que construye aplicaciones robustas y mantenibles que continúan funcionando mientras evolucionan. Esta es la marca distintiva del desarrollo profesional de OWL.
Recuerda: cada error que detectes en una prueba es un error que tus usuarios nunca verán. Cada técnica de depuración que domines es tiempo ahorrado en futuras investigaciones. Invierte en estas habilidades, y te pagarán dividendos a lo largo de tu carrera profesional.
TL;DR: TDD, mocking de servicios, CI y depuración segura en producción son lo que separa "funciona" de "sigue funcionando" — invierte en ellos en cuanto un componente hace algo de lo que un cliente realmente depende.
Pruébalo tú mismo: El ejemplo de este capítulo está disponible como addon instalable de Odoo 19: simplifyit_owl_book_ch16_2_ex1. Las instrucciones de instalación están en el README del repositorio.
¿Qué Sigue?
Pruebas y depuración asumen que sigues escribiendo OWL 2.0 puro. El Capítulo 17 Parte 1 cubre la realidad de la mayoría de los proyectos de Odoo: puentear componentes OWL nuevos con el sistema de widgets legado que todavía corre en producción.