Skip to Content
  • Home
  • Book
  • Blog
  • About us
  • Help
  • Contact us
  •  [email protected]
  • Sign in
  • English (US) Español (BO)
  • Simplify It S.R.L.
    • Contact Us
Simplify It S.R.L.
      • Home
      • Book
      • Blog
      • About us
      • Help
      • Contact us
    •  [email protected]
    • English (US) Español (BO)
    • Sign in
    • Contact Us

    Chapter 16 Part 2: Advanced Testing, TDD, and Production Debugging

  • All Blogs
  • OWL Book
  • Chapter 16 Part 2: Advanced Testing, TDD, and Production Debugging
  • August 25, 2026 by
    Chapter 16 Part 2: Advanced Testing, TDD, and Production Debugging
    Grover Menacho

    Why this chapter? Real projects eventually hit the issues that don't show up in a quick manual check — a memory leak that only appears after weeks of runtime, a flaky test that erodes trust in the whole suite, a bug that only reproduces under production load. This is the tooling for that stage of a project's life. What is Odoo trying to solve with this? Giving teams a way to test integration across services, wire up CI so regressions are caught before deploy, and debug production issues safely — instead of ad hoc console.log sessions on a client's live server. Real-world application: Diagnosing a memory leak a client reports after a month of daily use, or setting up CI so every merge is tested automatically before it reaches their instance.

    In Part 1, we covered the fundamentals of testing and debugging OWL components: writing your first tests, testing components that depend on services, using browser DevTools effectively, and following testing best practices. Now we'll go further: advanced testing patterns, Test-Driven Development, performance and integration testing, continuous integration, pitfalls that trip up even experienced developers, and how to debug issues once your code is running in production.


    Advanced Testing Patterns

    1. Testing Component Communication

    When testing parent-child component communication:

    /** @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("Counter integration", () => {
        test("parent-child communication", async () => {
            const receivedEvents = [];
    
            // Create a parent component that uses our tested component
            class TestParent extends Component {
                static template = xml`
                    <div>
                        <Counter initialValue="5" onValueChange.bind="onCounterChange"/>
                        <p class="parent-display">Parent sees: <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);
    
            // Interact with the child component
            await click(".increment-btn");
    
            // Verify parent received the event
            expect(receivedEvents).toEqual([6]);
            expect(".parent-display").toHaveText("Parent sees: 6");
        });
    });
    

    2. Testing with Props Changes

    Test how components react to prop changes. Since props always flow down from a parent in OWL, the cleanest way to test this is to wrap the component in a small test parent and change what it passes down — the same pattern used above:

    describe("Counter integration", () => {
        test("reacts to prop changes", async () => {
            class TestParent extends Component {
                static template = xml`<Counter max="state.max"/>`;
                static components = { Counter };
                setup() {
                    this.state = useState({ max: 5 });
                }
            }
    
            await mountWithCleanup(TestParent);
    
            // Increment to max
            for (let i = 0; i < 5; i++) {
                await click(".increment-btn");
            }
            expect(".increment-btn").toHaveProperty("disabled", true);
    
            // Raise the max on the parent's state; OWL re-renders the child with new props
            // Note: this snippet only illustrates the idea — TestParent's state isn't
            // reachable from outside, so in a real test you'd expose a way to update it
            // (e.g. a button in TestParent's template) and click that instead.
        });
    });
    

    3. Testing Async Operations

    For components with async operations:

    describe("TaskManager integration", () => {
        test("handles async operations", async () => {
            let resolvePromise;
            const asyncPromise = new Promise((resolve) => {
                resolvePromise = resolve;
            });
            onRpc("project.task", "search_read", () => asyncPromise);
    
            await mountWithCleanup(TaskManager);
    
            // Check loading state
            expect(".task-list p").toHaveCount(1);
    
            // Resolve the async operation
            resolvePromise([]);
            await animationFrame(); // let OWL flush the re-render after the promise resolves
    
            // Check that loading is gone
            expect(".task-list p").toHaveCount(0);
        });
    });
    

    Add import { animationFrame } from "@odoo/hoot-dom"; at the top — it's the general-purpose "wait a tick for the DOM to catch up" helper in hoot, used whenever you resolve a promise manually instead of going through click() or edit() (which already wait internally).


    Test-Driven Development (TDD) with OWL

    Test-Driven Development is a powerful approach where you write tests before implementing functionality:

    1. Red-Green-Refactor Cycle

    Red: Write a failing test

    test("counter displays correctly", async () => {
        await mountWithCleanup(Counter);
        expect(".counter-value").toHaveText("0");
    });
    

    Green: Write minimal code to make it pass

    export class Counter extends Component {
        static template = xml`
            <div>
                <span class="counter-value">0</span>
            </div>
        `;
    }
    

    Refactor: Improve the code while keeping tests green

    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. Benefits of TDD

    • Clear Requirements: Tests serve as specifications
    • Better Design: Writing tests first leads to more testable, modular code
    • Regression Protection: Comprehensive test suite catches future breakages
    • Confidence: Green tests mean working software

    Performance Testing and Debugging

    1. Measuring Component Performance

    test("counter performance with many updates", async () => {
        await mountWithCleanup(Counter, { props: { max: 1000 } });
    
        const startTime = performance.now();
    
        // Simulate rapid clicking
        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. Memory Leak Detection

    This one deliberately manages mount/destroy by hand instead of mountWithCleanup(), since the point of the test is to verify that manual cleanup doesn't leave anything behind — so it imports mount straight from @odoo/owl:

    import { mount } from "@odoo/owl";
    
    test("counter cleans up properly", async () => {
        const initialComponents = document.querySelectorAll(".counter-widget").length;
        const container = document.createElement("div");
    
        // Create and destroy multiple components
        for (let i = 0; i < 10; i++) {
            const counter = await mount(Counter, container);
            counter.destroy();
            container.innerHTML = ""; // Clear the container
        }
    
        // Force garbage collection if available
        if (window.gc) {
            window.gc();
        }
    
        const finalComponents = document.querySelectorAll(".counter-widget").length;
        expect(finalComponents).toBe(initialComponents);
    });
    

    Integration Testing

    Testing Components in Real Scenarios

    Sometimes you need to test how components work together in realistic conditions:

    test("counter works in form context", async () => {
        // Create a realistic parent component
        class TestForm extends Component {
            static template = xml`
                <form t-on-submit.prevent="onSubmit">
                    <div class="form-group">
                        <label>Quantity:</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">Order</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() {
                // Form submission logic
            }
        }
    
        await mountWithCleanup(TestForm);
    
        // Test integrated behavior
        await click(".increment-btn");
    
        expect(".counter-value").toHaveText("2");
        expect(".form-group:nth-child(2)").toHaveText("Total: $20");
    });
    

    Continuous Integration and Testing

    1. Running Tests in CI/CD

    Create a test script for your module:

    File: my_module/tests/test_js.py

    import odoo.tests
    
    
    @odoo.tests.tagged('post_install', '-at_install')
    class TestJavaScript(odoo.tests.HttpCase):
    
        def test_js_modules(self):
            """Test that JavaScript modules load and hoot unit tests pass"""
            self.browser_js(
                "/web/tests?module=my_module&failfast",
                code="",
                timeout=60,
            )
    

    2. Test Coverage Reporting

    Coverage for hoot tests is enabled from the test runner itself (e.g. a coverage=1 URL parameter on /web/tests), not from inside the test file. Once a coverage-enabled run finishes, the standard Istanbul coverage object is available on the page for inspection or export:

    if (window.__coverage__) {
        console.log("Coverage data:", window.__coverage__);
    }
    

    Common Testing Pitfalls and Solutions

    1. Flaky Tests

    Problem: Tests that sometimes pass and sometimes fail

    Solution: Ensure proper async handling

    // Bad - not waiting for async operations
    test("flaky test", async () => {
        await mountWithCleanup(Counter);
        click(".increment-btn"); // Not awaited!
        expect(".counter-value").toHaveText("1");
    });
    
    // Good - properly awaiting async operations
    test("reliable test", async () => {
        await mountWithCleanup(Counter);
        await click(".increment-btn"); // hoot's click() waits for the re-render itself
        expect(".counter-value").toHaveText("1");
    });
    

    2. Testing Private Methods

    Problem: Wanting to test internal component methods

    Solution: Test through public interface

    // Bad - testing private methods
    test("_calculateTotal works", async () => {
        const component = await mountWithCleanup(MyComponent);
        const result = component._calculateTotal(5, 10);
        expect(result).toBe(50);
    });
    
    // Good - testing through public behavior
    test("displays correct total when quantity changes", async () => {
        await mountWithCleanup(MyComponent);
        await edit(".quantity-input", "5");
        expect(".total").toHaveText("Total: 50");
    });
    

    3. Over-mocking

    Problem: Mocking too many dependencies

    Solution: Mock only what's necessary

    // Bad - over-mocking every service the component happens to touch
    mockService("orm", mockOrm);
    mockService("notification", mockNotification);
    mockService("user", mockUser);
    mockService("company", mockCompany);
    // ... mocking everything makes the test brittle and hides real integration bugs
    
    // Good - mock only the one RPC call this test actually cares about
    onRpc("my.model", "my_specific_method", () => mockData);
    // every other request goes through normal handling
    

    Debugging Production Issues

    1. Error Boundaries and Logging

    export class ErrorBoundary extends Component {
        static template = xml`
            <div>
                <t t-if="state.hasError">
                    <div class="alert alert-danger">
                        <h4>Something went wrong</h4>
                        <p t-esc="state.error.message"/>
                        <button t-on-click="retry">Try Again</button>
                    </div>
                </t>
                <t t-else="">
                    <t t-slot="default"/>
                </t>
            </div>
        `;
    
        setup() {
            this.state = useState({
                hasError: false,
                error: null
            });
        }
    
        catchError(error) {
            console.error("Component error caught:", error);
    
            // Send to error reporting service
            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 for Safe Deployments

    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")) {
                // New implementation
                this.state.count += this.props.step || 1;
            } else {
                // Safe fallback
                this.state.count++;
            }
        }
    }
    

    Exercises

    Exercise 1: Write a TDD Cycle

    Following the Red-Green-Refactor cycle, write a failing test for a new doubled getter on Counter that returns state.count * 2, implement the minimal code to make it pass, then refactor if you see room for improvement.

    Exercise 2: Mock a Service and Assert on Its Arguments

    Write a test for TaskManager.deleteTask that mocks the orm service directly with mockService("orm", ...) (instead of going through onRpc) and asserts that orm.unlink was called with ["project.task", [taskId]].

    Exercise 3: Sketch a CI Check

    Sketch (in comments — no need to run it) what you would add to test_js.py so CI fails the build if any hoot test in your module fails. In 2-3 sentences, explain why running JS tests in CI matters even when they already pass on your machine.


    Summary: Building Robust OWL Applications

    Testing and debugging are not afterthoughts—they're integral parts of building professional OWL applications. Here's your roadmap to excellence:

    1. Testing Strategy

    • Unit Tests: Test individual components in isolation
    • Integration Tests: Test component interactions
    • End-to-End Tests: Test complete user workflows
    • Performance Tests: Ensure acceptable response times
    • Error Handling Tests: Verify graceful failure modes

    2. Debugging Toolkit

    • Browser DevTools: Your primary debugging interface
    • Strategic Logging: Capture important state changes
    • Error Boundaries: Graceful error handling
    • Performance Monitoring: Track and optimize slow operations

    3. Professional Practices

    • Test-Driven Development: Write tests before implementation
    • Continuous Integration: Automated test running
    • Code Coverage: Ensure adequate test coverage
    • Documentation: Clear, executable specifications

    4. Maintenance Mindset

    • Refactor with Confidence: Good tests enable safe changes
    • Regression Prevention: Tests catch unexpected breakages
    • Living Documentation: Tests show how components should work
    • Team Collaboration: Shared understanding through tests

    By mastering these testing and debugging techniques, you transform from someone who writes code that works today into someone who builds robust, maintainable applications that continue working as they evolve. This is the hallmark of professional OWL development.

    Remember: every bug you catch in a test is a bug your users will never see. Every debugging technique you master is time saved in future investigations. Invest in these skills, and they'll pay dividends throughout your entire development career.

    TL;DR: TDD, service mocking, CI, and production-safe debugging are what separate "it works" from "it keeps working" — invest in them once a component is doing anything a client actually depends on.

    Try it yourself: This chapter's example is available as an installable Odoo 19 addon: simplifyit_owl_book_ch16_2_ex1. Installation instructions are in the repository README.

    What's Next?

    Testing and debugging assume you're still writing pure OWL 2.0. Chapter 17 Part 1 covers the reality of most Odoo projects: bridging new OWL components with the legacy widget system still running in production.

    in OWL Book
    Chapter 17 Part 1: The Legacy-OWL Bridge - Understanding and Basic Implementation
    Reading the whole book? Take it with you — free, no sign-up.
    PDF EPUB All chapters
    Useful Links
    • Home
    • About us
    • Blog
    • The OWL 2.0 book
    • Help
    • Contact us
    About us

    Simplify It S.R.L. is an Odoo implementation and development firm based in La Paz, Bolivia, working with companies across Latin America and North America. We have been building on Odoo since version 6.1: custom modules, version migrations and Bolivian localization.

    We are also the authors of the OWL 2.0 book, published free chapter by chapter on our blog.

    Connect with us
    • Contact us
    • [email protected]
    • +591 65144144
    • La Paz, Bolivia
    Follow us
    Copyright © Simplify It S.R.L.
    English (US) | Español (BO)
    Powered by Odoo - Create a free website