Ir al contenido
  • Inicio
  • Libro
  • Blog
  • Sobre nosotros
  • Ayuda
  • Contáctanos
  •  [email protected]
  • Inicia sesión
  • English (US) Español (BO)
  • Simplify It S.R.L.
    • Contáctanos
Simplify It S.R.L.
      • Inicio
      • Libro
      • Blog
      • Sobre nosotros
      • Ayuda
      • Contáctanos
    •  [email protected]
    • English (US) Español (BO)
    • Inicia sesión
    • Contáctanos

    Capítulo 16 Parte 1: Fundamentos de Pruebas y Depuración

  • Todos los blogs
  • Libro OWL
  • Capítulo 16 Parte 1: Fundamentos de Pruebas y Depuración
  • 25 de agosto de 2026 por
    Capítulo 16 Parte 1: Fundamentos de Pruebas y Depuración
    Grover Menacho

    ¿Por qué este capítulo? "Funciona en mi máquina" no alcanza cuando eres tú quien responde por la instancia de producción de un cliente — las pruebas son lo que te permite tocar código con confianza después de la primera entrega, en vez de temerle a cada cambio. ¿Qué trata de resolver Odoo con esto? Un framework de pruebas JS que pueda levantar un entorno Odoo simulado con los servicios correctos rápidamente, así probar un addon pequeño no requiere un servidor y una base de datos completos corriendo. Aplicación en la vida real: Detectar una regresión en una revisión de código, antes de que una actualización de rutina rompa un dashboard que el equipo de ventas de un cliente usa todas las mañanas.

    Escribir código que funciona hoy es bueno. Escribir código que puedas modificar con confianza mañana sin romper todo es excepcional. La diferencia radica en tener una estrategia integral de pruebas y dominar las técnicas de depuración.

    Las pruebas no son solo para encontrar errores: se tratan de diseñar mejores componentes, documentar el comportamiento esperado y crear una red de seguridad que te permita refactorizar y mejorar tu código con confianza. Cuando se combinan con técnicas efectivas de depuración, las pruebas transforman el desarrollo de una serie de experimentos esperanzadores en un proceso metódico y predecible.

    Odoo proporciona un poderoso framework de pruebas JavaScript construido sobre @odoo/hoot (el runner de pruebas unitarias moderno, usado desde Odoo 17.4, que reemplazó al framework anterior basado en QUnit), específicamente diseñado para probar componentes OWL en condiciones realistas. Esta parte del capítulo te lleva desde escribir tu primera prueba simple hasta probar componentes que dependen de servicios, además de las herramientas de depuración y las buenas prácticas que usarás día a día.


    Por qué Importan las Pruebas en el Desarrollo OWL

    Antes de sumergirnos en el "cómo", entendamos el "por qué" de probar componentes OWL.

    La Realidad de la Interdependencia de Componentes

    Los componentes OWL no existen de forma aislada. Ellos: - Reciben props de los padres y pasan props a los hijos - Emiten eventos que los padres escuchan - Usan servicios para interactuar con el backend de Odoo - Manipulan el DOM a través de plantillas - Administran el estado local que afecta el renderizado

    Cada una de estas interacciones es un punto potencial de falla. Sin pruebas, básicamente estás volando a ciegas, haciendo cambios y esperando que nada se rompa.

    El Costo de las Pruebas Manuales

    Considera un flujo de trabajo típico de desarrollo sin pruebas automatizadas:

    1. Hacer un cambio en un componente
    2. Iniciar el servidor de Odoo (30-60 segundos)
    3. Navegar a la página donde se usa el componente
    4. Interactuar manualmente con el componente para verificar que funciona
    5. Verificar casos límite creando manualmente diferentes escenarios
    6. Repetir para cada componente que podría verse afectado

    Para un solo cambio pequeño, este proceso podría tomar 5-10 minutos. Multiplica eso por docenas de cambios por día, y estás gastando horas en pruebas manuales repetitivas.

    Los Beneficios de las Pruebas Automatizadas

    Las pruebas automatizadas cambian esto completamente:

    • Velocidad: Las pruebas se ejecutan en segundos, no en minutos
    • Consistencia: Las pruebas verifican las mismas cosas cada vez, de la misma manera
    • Cobertura: Las pruebas pueden verificar casos límite que podrías olvidar manualmente
    • Confianza: Las pruebas verdes significan que tus cambios no han roto la funcionalidad existente
    • Documentación: Las pruebas sirven como ejemplos ejecutables de cómo deben comportarse los componentes

    Configurando tu Entorno de Pruebas

    1. Organización de Archivos de Prueba

    Odoo sigue una convención clara para organizar archivos de prueba:

    mi_modulo/
    |-- static/src/
    |   |-- components/
    |   |   |-- counter/
    |   |   |   |-- counter.js
    |   |   |   |-- counter.xml
    |   |   |   |-- tests/
    |   |   |       |-- counter.test.js
    |   |   |-- task_list/
    |   |       |-- task_list.js
    |   |       |-- task_list.xml
    |   |       |-- tests/
    |   |           |-- task_list.test.js
    |   |-- services/
    |       |-- theme_store.js
    |       |-- tests/
    |           |-- theme_store.test.js
    

    Esta organización mantiene las pruebas cerca del código que están probando, haciéndolas fáciles de encontrar y mantener.

    2. Estructura Básica de Archivo de Prueba

    Cada archivo de prueba JavaScript de Odoo sigue este patrón:

    /** @odoo-module **/
    
    import { describe, test, beforeEach } from "@odoo/hoot";
    import { mountWithCleanup } from "@web/../tests/web_test_helpers";
    
    // Importar el componente que estás probando
    import { Counter } from "../counter";
    
    describe("Counter", () => {
        beforeEach(() => {
            // Se ejecuta antes de cada prueba en este bloque describe()
        });
    
        test("renderizado básico", async () => {
            // La implementación de la prueba va aquí
        });
    });
    

    Helpers clave explicados: - describe(): Agrupa pruebas relacionadas, como una suite de pruebas - test(): Define un caso de prueba individual - beforeEach(): Ejecuta código de configuración antes de cada prueba dentro del describe() que la contiene - mountWithCleanup(): Monta un componente en la página de pruebas y lo desmonta automáticamente al terminar la prueba — sin necesidad de gestionar target/env/.destroy() a mano como con el framework antiguo basado en QUnit

    3. Agregando Pruebas a tu Módulo

    No olvides incluir tus archivos de prueba en __manifest__.py:

    {
        'name': 'Mi Módulo',
        # ... otros datos del manifest ...
        'assets': {
            'web.assets_backend': [
                'mi_modulo/static/src/components/**/*.js',
                'mi_modulo/static/src/components/**/*.xml',
            ],
            'web.assets_unit_tests': [
                'mi_modulo/static/src/components/**/tests/*.test.js',
            ],
        },
    }
    

    Escribiendo tu Primera Prueba de Componente

    Comencemos con un ejemplo simple pero realista: probar un componente Counter.

    El Componente Counter

    Primero, aquí está el componente que vamos a probar:

    Archivo: counter.js

    /** @odoo-module **/
    
    import { Component, useState } from "@odoo/owl";
    
    export class Counter extends Component {
        static template = xml`
            <div class="counter-widget">
                <h3>Contador: <span class="counter-value" t-esc="state.count"/></h3>
                <div class="counter-controls">
                    <button class="btn btn-secondary decrement-btn" 
                            t-on-click="decrement"
                            t-att-disabled="state.count <= props.min">
                        -
                    </button>
                    <button class="btn btn-primary reset-btn" t-on-click="reset">
                        Reiniciar
                    </button>
                    <button class="btn btn-secondary increment-btn" 
                            t-on-click="increment"
                            t-att-disabled="state.count >= props.max">
                        +
                    </button>
                </div>
                <div class="counter-info" t-if="showInfo">
                    <small>Mín: <t t-esc="props.min"/>, Máx: <t t-esc="props.max"/></small>
                </div>
            </div>
        `;
    
        static props = {
            initialValue: { type: Number, optional: true },
            min: { type: Number, optional: true },
            max: { type: Number, optional: true },
            onValueChange: { type: Function, optional: true },
        };
    
        static defaultProps = {
            initialValue: 0,
            min: 0,
            max: 100,
        };
    
        setup() {
            this.state = useState({
                count: this.props.initialValue,
            });
        }
    
        get showInfo() {
            return this.props.min !== 0 || this.props.max !== 100;
        }
    
        increment() {
            if (this.state.count < this.props.max) {
                this.state.count++;
                this._notifyChange();
            }
        }
    
        decrement() {
            if (this.state.count > this.props.min) {
                this.state.count--;
                this._notifyChange();
            }
        }
    
        reset() {
            this.state.count = this.props.initialValue;
            this._notifyChange();
        }
    
        _notifyChange() {
            if (this.props.onValueChange) {
                this.props.onValueChange(this.state.count);
            }
        }
    }
    

    Suite de Pruebas Integral

    Ahora escribamos pruebas exhaustivas para este componente. Las construiremos de forma incremental, unas pocas a la vez, para que sea más fácil ver qué verifica cada grupo.

    Archivo: counter.test.js

    Primero, las pruebas de renderizado — verifican que el componente muestra lo correcto según distintas props:

    /** @odoo-module **/
    
    import { describe, test, expect } from "@odoo/hoot";
    import { mountWithCleanup } from "@web/../tests/web_test_helpers";
    
    import { Counter } from "../counter";
    
    describe("Counter", () => {
        test("renderiza con props por defecto", async () => {
            // Montar componente sin props (debería usar valores por defecto)
            await mountWithCleanup(Counter);
    
            // Verificar visualización inicial
            expect(".counter-value").toHaveText("0");
    
            // Verificar que los botones existen
            expect(".increment-btn").toHaveCount(1);
            expect(".decrement-btn").toHaveCount(1);
            expect(".reset-btn").toHaveCount(1);
    
            // Verificar que la info no se muestra (min/max por defecto)
            expect(".counter-info").toHaveCount(0);
        });
    
        test("renderiza con props personalizadas", async () => {
            await mountWithCleanup(Counter, {
                props: {
                    initialValue: 5,
                    min: 2,
                    max: 8,
                },
            });
    
            // Verificar valor inicial personalizado
            expect(".counter-value").toHaveText("5");
    
            // Verificar que se muestra la info, con los valores min/max personalizados
            expect(".counter-info").toHaveCount(1);
            expect(".counter-info").toHaveText("Mín: 2, Máx: 8");
        });
    });
    

    Matchers clave explicados: - expect(selector).toHaveText(texto): Afirma que el elemento que coincide con selector tiene exactamente ese texto - expect(selector).toHaveCount(n): Afirma que exactamente n elementos coinciden con selector (usa 0 para afirmar que algo está ausente)

    A continuación, las pruebas de interacción — hacer clic en los botones de incremento, decremento y reinicio, y verificar que se respetan los límites min/max. Se agregan dentro del mismo bloque describe("Counter", ...) mostrado arriba, y usan click() de @odoo/hoot-dom, que simula un clic real de usuario y espera a que OWL termine de re-renderizar antes de resolver:

        test("funcionalidad de incremento", async () => {
            await mountWithCleanup(Counter, {
                props: { initialValue: 0, max: 3 },
            });
    
            // Probar incremento normal
            await click(".increment-btn");
            expect(".counter-value").toHaveText("1");
    
            await click(".increment-btn");
            expect(".counter-value").toHaveText("2");
    
            await click(".increment-btn");
            expect(".counter-value").toHaveText("3");
    
            // Probar que el botón se desactiva en el máximo
            expect(".increment-btn").toHaveProperty("disabled", true);
    
            // Hacer clic en un botón desactivado no hace nada
            await click(".increment-btn");
            expect(".counter-value").toHaveText("3");
        });
    
        test("funcionalidad de decremento", async () => {
            await mountWithCleanup(Counter, {
                props: { initialValue: 3, min: 1 },
            });
    
            // Probar decremento normal
            await click(".decrement-btn");
            expect(".counter-value").toHaveText("2");
    
            await click(".decrement-btn");
            expect(".counter-value").toHaveText("1");
    
            // Probar que el botón se desactiva en el mínimo
            expect(".decrement-btn").toHaveProperty("disabled", true);
    
            // Hacer clic en un botón desactivado no hace nada
            await click(".decrement-btn");
            expect(".counter-value").toHaveText("1");
        });
    
        test("funcionalidad de reinicio", async () => {
            await mountWithCleanup(Counter, {
                props: { initialValue: 5, min: 0, max: 10 },
            });
    
            // Cambiar el valor
            await click(".increment-btn");
            await click(".increment-btn");
            expect(".counter-value").toHaveText("7");
    
            // Probar reinicio
            await click(".reset-btn");
            expect(".counter-value").toHaveText("5");
        });
    

    Recuerda agregar import { click } from "@odoo/hoot-dom"; junto al import de @odoo/hoot al principio del archivo.

    Por último, las pruebas de callback y de casos límite — verifican que onValueChange se dispara con los valores correctos, y que combinaciones inusuales de props (como min === max) no rompen el componente. Mismo bloque describe("Counter", ...) de arriba, cerrado al final. Nota que cada escenario tiene su propio test() — siguiendo la práctica de "mantener las pruebas independientes" que se cubre más adelante en este capítulo, en vez de amontonar varios montajes en una sola prueba:

        test("callback onValueChange", async () => {
            const valueChanges = [];
            const onValueChange = (newValue) => {
                valueChanges.push(newValue);
            };
    
            await mountWithCleanup(Counter, {
                props: {
                    initialValue: 2,
                    onValueChange,
                },
            });
    
            // Probar callback de incremento
            await click(".increment-btn");
            // Probar callback de decremento
            await click(".decrement-btn");
            // Probar callback de reinicio
            await click(".reset-btn");
    
            // incrementar a 3, decrementar a 2, reiniciar a 2
            expect(valueChanges).toEqual([3, 2, 2]);
        });
    
        test("incremento y decremento están desactivados cuando min === max", async () => {
            await mountWithCleanup(Counter, {
                props: { initialValue: 5, min: 5, max: 5 },
            });
    
            expect(".increment-btn").toHaveProperty("disabled", true);
            expect(".decrement-btn").toHaveProperty("disabled", true);
        });
    
        test("el valor inicial fuera de límites se preserva", async () => {
            // Nota: En una implementación real, podrías querer limitar el valor inicial.
            // Esta prueba documenta el comportamiento actual.
            await mountWithCleanup(Counter, {
                props: { initialValue: 100, min: 1, max: 10 },
            });
    
            expect(".counter-value").toHaveText("100");
        });
    });
    

    Matchers clave explicados: - expect(valor).toEqual(otro): Afirma igualdad profunda entre dos valores (arrays, objetos) — usa .toBe() en vez de esto para primitivos como strings, números y booleanos - expect(selector).toHaveProperty(nombre, valor): Afirma que una propiedad del DOM (como disabled) del elemento coincidente es igual a valor


    Probando Componentes con Servicios

    Muchos componentes del mundo real dependen de servicios de Odoo. Así es como probarlos.

    Componente que Usa Servicios

    Este componente TaskManager carga tareas con el servicio orm al montarse, y muestra retroalimentación con el servicio notification. Veamos primero la mitad que carga los datos:

    /** @odoo-module **/
    
    import { Component, useState } from "@odoo/owl";
    import { useService } from "@web/core/utils/hooks";
    
    export class TaskManager extends Component {
        static template = xml`
            <div class="task-manager">
                <h3>Mis Tareas</h3>
                <div class="task-input">
                    <input type="text" 
                           t-model="state.newTaskText"
                           placeholder="Agregar una nueva tarea..."
                           t-on-keydown="onKeydown"/>
                    <button class="btn btn-primary" 
                            t-on-click="addTask"
                            t-att-disabled="!state.newTaskText">
                        Agregar Tarea
                    </button>
                </div>
    
                <div class="task-list" t-if="state.loading">
                    <p>Cargando tareas...</p>
                </div>
    
                <div class="task-list" t-else="">
                    <div t-foreach="state.tasks" t-as="task" t-key="task.id" 
                         class="task-item"
                         t-att-class="{ 'completed': task.completed }">
                        <input type="checkbox" 
                               t-att-checked="task.completed"
                               t-on-change="(ev) => this.toggleTask(task.id)"/>
                        <span class="task-text" t-esc="task.text"/>
                        <button class="btn btn-sm btn-danger delete-btn" 
                                t-on-click="() => this.deleteTask(task.id)">
                            Eliminar
                        </button>
                    </div>
                </div>
            </div>
        `;
    
        setup() {
            this.orm = useService("orm");
            this.notification = useService("notification");
    
            this.state = useState({
                tasks: [],
                newTaskText: "",
                loading: true,
            });
    
            this.loadTasks();
        }
    
        async loadTasks() {
            this.state.loading = true;
            try {
                const tasks = await this.orm.searchRead(
                    "project.task",
                    [["user_id", "=", this.orm.user.userId]],
                    ["id", "name", "stage_id"]
                );
    
                this.state.tasks = tasks.map(task => ({
                    id: task.id,
                    text: task.name,
                    completed: task.stage_id[1] === "Done"
                }));
            } catch (error) {
                this.notification.add("Error al cargar tareas", { type: "danger" });
            } finally {
                this.state.loading = false;
            }
        }
    

    Y el resto de la clase — agregar, marcar y eliminar tareas, más el atajo de la tecla Enter. Sigue siendo la misma clase TaskManager, continuada:

        async addTask() {
            if (!this.state.newTaskText.trim()) return;
    
            try {
                const [taskId] = await this.orm.create("project.task", {
                    name: this.state.newTaskText,
                    user_id: this.orm.user.userId,
                });
    
                this.state.tasks.push({
                    id: taskId,
                    text: this.state.newTaskText,
                    completed: false
                });
    
                this.state.newTaskText = "";
                this.notification.add("Tarea agregada exitosamente", { type: "success" });
            } catch (error) {
                this.notification.add("Error al agregar tarea", { type: "danger" });
            }
        }
    
        async toggleTask(taskId) {
            const task = this.state.tasks.find(t => t.id === taskId);
            if (!task) return;
    
            try {
                // En una implementación real, actualizarías el stage_id
                task.completed = !task.completed;
                this.notification.add(
                    task.completed ? "¡Tarea completada!" : "Tarea reabierta", 
                    { type: "info" }
                );
            } catch (error) {
                // Revertir en caso de error
                task.completed = !task.completed;
                this.notification.add("Error al actualizar tarea", { type: "danger" });
            }
        }
    
        async deleteTask(taskId) {
            try {
                await this.orm.unlink("project.task", [taskId]);
                this.state.tasks = this.state.tasks.filter(t => t.id !== taskId);
                this.notification.add("Tarea eliminada", { type: "info" });
            } catch (error) {
                this.notification.add("Error al eliminar tarea", { type: "danger" });
            }
        }
    
        onKeydown(event) {
            if (event.key === "Enter") {
                this.addTask();
            }
        }
    }
    

    Probando con Servicios Mock

    Para probar TaskManager sin un servidor Odoo real, hacemos mock de la capa RPC en lugar de golpear la base de datos, usando el helper onRpc() para interceptar llamadas ORM específicas. Primero, las pruebas que cubren cargar datos y agregar una nueva tarea:

    /** @odoo-module **/
    
    import { describe, test, expect } from "@odoo/hoot";
    import { click, edit, queryAllTexts } from "@odoo/hoot-dom";
    import { mountWithCleanup, onRpc } from "@web/../tests/web_test_helpers";
    
    import { TaskManager } from "../task_manager";
    
    describe("TaskManager", () => {
        test("carga y muestra tareas", async () => {
            // Mock de search_read del ORM para el modelo project.task
            const mockTasks = [
                { id: 1, name: "Comprar comida", stage_id: [1, "Por Hacer"] },
                { id: 2, name: "Pasear al perro", stage_id: [2, "Completado"] },
                { id: 3, name: "Escribir pruebas", stage_id: [1, "Por Hacer"] },
            ];
            onRpc("project.task", "search_read", () => mockTasks);
    
            await mountWithCleanup(TaskManager);
    
            // Verificar que el estado de carga se fue
            expect(".task-list p").toHaveCount(0);
    
            // Verificar que las tareas se muestran
            expect(".task-item").toHaveCount(3);
            expect(queryAllTexts(".task-text")).toEqual([
                "Comprar comida",
                "Pasear al perro",
                "Escribir pruebas",
            ]);
    
            // Verificar estado completado (la segunda tarea tiene stage_id "Completado")
            expect(".task-item:nth-child(2)").toHaveClass("completed");
        });
    
        test("agrega una nueva tarea", async () => {
            let createdTask = null;
            onRpc("project.task", "search_read", () => []); // Comenzar con lista de tareas vacía
            onRpc("project.task", "create", ({ args }) => {
                createdTask = args[0]; // Capturar datos de la tarea creada
                return 123; // ID mock
            });
    
            await mountWithCleanup(TaskManager);
    
            // Escribir nueva tarea y hacer clic en agregar
            await edit(".task-input input", "Nueva tarea de prueba");
            await click(".btn-primary");
    
            // Verificar que se hizo la llamada ORM
            expect(createdTask).not.toBe(null);
            expect(createdTask.name).toBe("Nueva tarea de prueba");
    
            // Verificar que la tarea aparece en la UI
            expect(".task-item").toHaveCount(1);
            expect(queryAllTexts(".task-text")).toEqual(["Nueva tarea de prueba"]);
    
            // Verificar que el input se limpia
            expect(".task-input input").toHaveValue("");
        });
    });
    

    Helpers clave explicados: - onRpc(modelo, método, callback): Intercepta una llamada ORM específica en vez de golpear el servidor real; el callback recibe { args, kwargs } (los argumentos que el componente pasó a orm.create/orm.write/etc.) y su valor de retorno se convierte en el resultado del RPC - edit(selector, valor): Escribe valor en el input coincidente y dispara los eventos que la escritura real de un usuario dispararía (de @odoo/hoot-dom) - queryAllTexts(selector): Devuelve el texto recortado de cada elemento que coincide con selector, como un array — útil para comparar toda una lista de una sola vez

    Las dos pruebas restantes verifican el manejo de errores y el atajo de teclado. Se agregan dentro del mismo bloque describe("TaskManager", ...) mostrado arriba, cerrado al final:

        test("maneja errores RPC elegantemente", async () => {
            const notificationMessages = [];
            onRpc("project.task", "search_read", () => {
                throw new Error("Error de red");
            });
            mockService("notification", {
                add: (message, options) => {
                    notificationMessages.push({ message, options });
                },
            });
    
            await mountWithCleanup(TaskManager);
    
            // Verificar que se mostró la notificación de error
            expect(notificationMessages.length).toBe(1);
            expect(notificationMessages[0].message).toBe("Error al cargar tareas");
            expect(notificationMessages[0].options.type).toBe("danger");
    
            // Verificar que el estado de carga se limpia incluso en caso de error
            expect(".task-list p").toHaveCount(0);
        });
    
        test("los atajos de teclado funcionan", async () => {
            let taskCreated = false;
            onRpc("project.task", "search_read", () => []);
            onRpc("project.task", "create", () => {
                taskCreated = true;
                return 456;
            });
    
            await mountWithCleanup(TaskManager);
    
            // Escribir texto de la tarea y presionar Enter
            await edit(".task-input input", "Tarea por tecla Enter");
            await press("Enter");
    
            expect(taskCreated).toBe(true);
        });
    });
    

    Agrega import { mockService } from "@web/../tests/web_test_helpers"; e import { press } from "@odoo/hoot-dom"; junto a los demás imports al inicio del archivo. mockService() reemplaza un servicio real de Odoo por una implementación falsa durante la prueba — aquí sustituye notification para poder capturar los mensajes que recibe en vez de mostrar toasts reales. press(tecla) simula presionar una tecla del teclado sobre el elemento que tiene el foco actualmente.


    Técnicas Efectivas de Depuración

    Aunque las pruebas detectan muchos problemas, aún necesitarás depurar problemas. Aquí están las estrategias profesionales de depuración:

    1. Usando las DevTools del Navegador Efectivamente

    Estableciendo Breakpoints Estratégicos:

    // En tu componente
    increment() {
        debugger; // La ejecución se pausará aquí
        if (this.state.count < this.props.max) {
            this.state.count++;
            this._notifyChange();
        }
    }
    

    Breakpoints Condicionales: Haz clic derecho en un número de línea en la pestaña Sources de DevTools y selecciona "Agregar breakpoint condicional":

    this.state.count > 5  // Solo pausar cuando count exceda 5
    

    Observando Variables: En el debugger de DevTools, agrega variables al panel "Watch" para ver cómo cambian mientras recorres el código.

    2. Logging Estratégico en la Consola

    Logging del Ciclo de Vida del Componente:

    setup() {
        console.log("Configuración del Counter", this.props);
        this.state = useState({ count: this.props.initialValue });
    }
    
    increment() {
        console.log("Antes del incremento:", this.state.count);
        if (this.state.count < this.props.max) {
            this.state.count++;
            console.log("Después del incremento:", this.state.count);
            this._notifyChange();
        } else {
            console.log("Incremento bloqueado - en el máximo:", this.props.max);
        }
    }
    

    Seguimiento de Cambios de Estado:

    setup() {
        this.state = useState({ count: this.props.initialValue });
    
        // Registrar todos los cambios de estado
        const originalState = this.state;
        Object.defineProperty(this, 'state', {
            get: () => originalState,
            set: (newState) => {
                console.log("Estado cambiando de", originalState, "a", newState);
                originalState = newState;
            }
        });
    }
    

    3. Herramientas de Inspección de Componentes

    OWL DevTools: Instala la extensión de navegador OWL DevTools para: - Ver la estructura del árbol de componentes - Inspeccionar props y estado de componentes - Rastrear actualizaciones y re-renderizados de componentes

    Accediendo Componentes desde la Consola: OWL no expone una forma pública y soportada de obtener la instancia de un componente montado a partir de un elemento del DOM — para eso está justamente la extensión OWL DevTools de arriba. Si necesitas acceso programático mientras desarrollas, expón la instancia tú mismo, de forma deliberada, como un atajo solo para depuración:

    // En el componente, solo durante el desarrollo:
    setup() {
        onMounted(() => {
            // Gancho de depuración intencional — nunca confíes en esto en producción.
            this.el.__debugComponent = this;
        });
    }
    
    // Luego, en la consola del navegador:
    const counterComponent = document.querySelector('.counter-widget').__debugComponent;
    console.log(counterComponent.state);
    console.log(counterComponent.props);
    

    4. Depurando Problemas Comunes

    Props No Se Actualizan:

    // Verificar si el padre realmente está pasando nuevas props
    setup() {
        onWillUpdateProps((nextProps) => {
            console.log("Props actuales:", this.props);
            console.log("Próximas props:", nextProps);
            console.log("Props cambiaron:", JSON.stringify(this.props) !== JSON.stringify(nextProps));
        });
    }
    

    Estado No Desencadena Re-renderizado:

    // Asegurarse de usar useState correctamente
    setup() {
        // Bueno - estado reactivo
        this.state = useState({ count: 0 });
    
        // Malo - no reactivo
        this.state = { count: 0 };
    }
    

    Manejadores de Eventos No Funcionan:

    // Verificar vinculación de eventos en la plantilla
    static template = xml`
        <!-- Bueno - vinculación apropiada -->
        <button t-on-click="increment">+</button>
    
        <!-- Malo - llamando función inmediatamente -->
        <button t-on-click="increment()">+</button>
    
        <!-- Bueno - función flecha para parámetros -->
        <button t-on-click="() => this.incrementBy(5)">+5</button>
    `;
    

    Mejores Prácticas de Pruebas

    1. Escribir Nombres Descriptivos de Pruebas

    // Malo - nombres de prueba vagos
    test("probar contador", async () => { ... });
    test("prueba de botón", async () => { ... });
    
    // Bueno - nombres de prueba descriptivos
    test("el contador muestra el valor inicial de las props", async () => { ... });
    test("el botón de incremento se desactiva en el valor máximo", async () => { ... });
    

    2. Probar Comportamiento, No Implementación

    // Malo - probando detalles de implementación
    test("state.count aumenta en 1", async () => {
        const counter = await mountWithCleanup(Counter);
        counter.state.count = 5; // Manipulación directa del estado
        expect(counter.state.count).toBe(5);
    });
    
    // Bueno - probando comportamiento visible para el usuario
    test("hacer clic en el botón de incremento aumenta el valor mostrado", async () => {
        await mountWithCleanup(Counter);
        await click(".increment-btn");
        expect(".counter-value").toHaveText("1");
    });
    

    3. Usar el Patrón Organizar-Actuar-Afirmar

    test("el botón de reinicio restaura el valor inicial", async () => {
        // Organizar
        await mountWithCleanup(Counter, { props: { initialValue: 5 } });
        await click(".increment-btn"); // Cambiar valor
    
        // Actuar
        await click(".reset-btn");
    
        // Afirmar
        expect(".counter-value").toHaveText("5");
    });
    

    4. Mantener las Pruebas Independientes

    // Malo - las pruebas dependen unas de otras
    let sharedCounter;
    
    test("configurar contador", async () => {
        sharedCounter = await mountWithCleanup(Counter);
        expect(sharedCounter).toBeTruthy();
    });
    
    test("incrementar contador", async () => {
        await click(".increment-btn"); // Depende de que la prueba anterior siga montada
        // ...
    });
    
    // Bueno - cada prueba es independiente
    test("el contador puede ser incrementado", async () => {
        await mountWithCleanup(Counter);
        await click(".increment-btn");
        expect(".counter-value").toHaveText("1");
    });
    

    5. Probar Casos Límite

    // Una prueba por escenario mantiene los fallos fáciles de identificar
    test("maneja valores extremos", async () => {
        await mountWithCleanup(Counter, {
            props: { initialValue: Number.MAX_SAFE_INTEGER, max: Number.MAX_SAFE_INTEGER },
        });
        expect(".counter-value").toHaveCount(1);
    });
    
    test("maneja rangos negativos", async () => {
        await mountWithCleanup(Counter, {
            props: { initialValue: -5, min: -10, max: 0 },
        });
        expect(".counter-value").toHaveText("-5");
    });
    
    test("maneja min igual a max", async () => {
        await mountWithCleanup(Counter, {
            props: { initialValue: 5, min: 5, max: 5 },
        });
        expect(".increment-btn").toHaveProperty("disabled", true);
    });
    

    Errores Comunes y Soluciones

    Error Común 1: Olvidar await en Interacciones Asíncronas

    // Malo - la aserción se ejecuta antes de que el DOM se haya vuelto a renderizar
    test("inestable", async () => {
        await mountWithCleanup(Counter);
        click(".increment-btn"); // ¡no se esperó!
        expect(".counter-value").toHaveText("1");
    });
    
    // Correcto - esperar el clic; click() de hoot solo resuelve después de que OWL re-renderiza
    test("confiable", async () => {
        await mountWithCleanup(Counter);
        await click(".increment-btn");
        expect(".counter-value").toHaveText("1");
    });
    

    Error Común 2: Componentes que Sobreviven entre Pruebas

    El antiguo framework basado en QUnit exigía llamar tú mismo a counter.destroy() al final de cada prueba, y olvidarlo dejaba un componente montado con el que la siguiente prueba tropezaba. mountWithCleanup() elimina este error de raíz — siempre desmonta el componente automáticamente cuando termina la prueba. El error ahora es recurrir por costumbre al mount() plano de OWL en su lugar:

    // Malo - el mount() plano de @odoo/owl nunca se limpia automáticamente
    import { mount } from "@odoo/owl";
    
    test("el incremento funciona", async () => {
        await mount(Counter, document.body);
        await click(".increment-btn");
        // este componente sigue montado cuando empieza la siguiente prueba
    });
    
    // Correcto - mountWithCleanup() siempre desmonta al terminar la prueba, pase o falle
    test("el incremento funciona", async () => {
        await mountWithCleanup(Counter);
        await click(".increment-btn");
    });
    

    Error Común 3: Recurrir a console.log en Vez del Inspector de Componentes

    Poner console.log por todos lados funciona, pero es lento de iterar y fácil de olvidar quitar después. Para cualquier cosa más allá de una verificación puntual rápida, instala la extensión OWL DevTools (ver arriba) e inspecciona state/props directamente sobre el árbol de componentes en vivo — es más rápido y nunca termina accidentalmente en producción.

    Error Común 4: Depurar Sin Leer el Stack Trace Completo

    Es tentador saltar directo a un debugger o a un console.log cuando algo se rompe. Primero lee el stack trace completo en la consola — normalmente indica el componente, método y línea exactos, lo que te dice dónde poner un breakpoint en vez de adivinar.


    Ejercicios

    Ejercicio 1: Probar un Nuevo Comportamiento

    Agrega una prop step a Counter (con valor por defecto 1) que controle cuánto cambian increment/decrement el conteo. Escribe una prueba que monte el componente con props: { step: 5 } y verifique que un solo clic en el botón de incremento mueve el valor de 0 a 5.

    Ejercicio 2: Hacer Mock de un Fallo de Servicio

    Usando TaskManager, escribe una prueba donde onRpc("project.task", "create", ...) lance un error (no solo search_read) y verifica que el componente muestra una notificación de "Error al agregar tarea" en vez de agregar la tarea a la lista.

    Ejercicio 3: Depurar un Manejador Roto

    Toma onKeydown y rómpelo intencionalmente comparando event.key === "enter" (minúscula) en vez de "Enter". Usa un breakpoint o logging en consola para encontrar el error antes de mirar la solución, y luego corrígelo.


    Ahora tienes los fundamentos: por qué importan las pruebas, cómo configurar tu entorno, cómo escribir y ejecutar pruebas para componentes con y sin servicios, cómo depurar problemas con herramientas del navegador, y las buenas prácticas que mantienen una suite de pruebas confiable. En la Parte 2 construiremos sobre esta base con patrones de pruebas avanzados, Desarrollo Dirigido por Pruebas, pruebas de rendimiento e integración, integración continua, y las técnicas que los equipos profesionales usan para depurar problemas que solo aparecen en producción.

    TL;DR: Escribe pruebas con @odoo/hoot usando mountWithCleanup, haz mock de los servicios de los que depende un componente, y usa las OWL DevTools del navegador en vez de esparcir console.log — esa combinación es lo que hace que cambiar código después sea seguro en vez de aterrador.

    Pruébalo tú mismo: El ejemplo de este capítulo está disponible como addon instalable de Odoo 19: simplifyit_owl_book_ch16_1_ex1. Las instrucciones de instalación están en el README del repositorio.

    ¿Qué Sigue?

    Ya puedes escribir y confiar en una suite de pruebas básica. El Capítulo 16 Parte 2 va más allá — patrones de pruebas avanzados, Desarrollo Dirigido por Pruebas, y las técnicas de depuración que necesitas cuando un bug solo aparece en producción.

    en Libro OWL
    Capítulo 16 Parte 2: Pruebas Avanzadas, TDD y Depuración en Producción
    Reading the whole book? Take it with you — free, no sign-up.
    PDF EPUB All chapters
    Enlaces útiles
    • Inicio
    • Sobre nosotros
    • Blog
    • El libro de OWL 2.0
    • Ayuda
    • Contáctanos
    Sobre nosotros

    Simplify It S.R.L. es una firma de implementación y desarrollo Odoo con base en La Paz, Bolivia, que trabaja con empresas de Latinoamérica y Norteamérica. Desarrollamos sobre Odoo desde la versión 6.1: módulos a medida, migraciones de versión y localización boliviana.

    También somos autores del libro de OWL 2.0, que publicamos gratis capítulo a capítulo en nuestro blog.

    Conecta con nosotros
    • Contáctanos
    • [email protected]
    • +591 65144144
    • La Paz, Bolivia
    Síguenos
    Copyright © Simplify It S.R.L.
    English (US) | Español (BO)
    Con la tecnología de Odoo - Cree un sitio web gratuito