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.logsessions 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.