Odoo · Testing · TDD
El Test-Driven Development (desarrollo guiado por pruebas) cambia el orden: primero el test que falla, luego el código mínimo que lo pasa, y después el refactor. En Odoo funciona — y te ahorra regresiones en cada -u.
Introducción
¿Por qué TDD en módulos Odoo?
Odoo es un framework enorme con herencias en cadena, triggers ORM, computed fields y cron jobs. Un cambio inocente en write() puede romper tres módulos aguas abajo. Los tests automatizados son tu red de seguridad; el TDD te obliga a escribirlos antes, lo que mejora el diseño del API interno de tu módulo.
«Si no está testeado, no funciona — solo no lo sabes todavía.»
Ciclo
Red → Green → Refactor
🔴 Red
Escribe un test que describe el comportamiento deseado. Falla porque el código no existe.
🟢 Green
Implementa lo mínimo para que el test pase. Sin optimizar, sin extras.
🔵 Refactor
Limpia el código con confianza. Los tests te avisan si rompes algo.
Ejemplo
TDD paso a paso: descuento por volumen
Imagina un módulo que aplica un 10% de descuento en pedidos de venta con más de 10 unidades totales.
🔴 Paso 1 — Test que falla
# mi_modulo/tests/test_sale_discount.py
from odoo.tests import TransactionCase
class TestSaleVolumeDiscount(TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.product = cls.env['product.product'].create({{
'name': 'Test Product',
'list_price': 100.0,
}})
cls.partner = cls.env['res.partner'].create({{
'name': 'Test Customer',
}})
def test_volume_discount_applied_above_threshold(self):
"""Orders with qty > 10 get 10% discount."""
order = self.env['sale.order'].create({{
'partner_id': self.partner.id,
'order_line': [(0, 0, {{
'product_id': self.product.id,
'product_uom_qty': 15,
}})],
}})
order.action_apply_volume_discount()
line = order.order_line[0]
self.assertEqual(line.discount, 10.0)
Ejecutas el test → falla porque action_apply_volume_discount no existe.
🟢 Paso 2 — Código mínimo
# mi_modulo/models/sale_order.py
from odoo import models
VOLUME_THRESHOLD = 10
VOLUME_DISCOUNT = 10.0
class SaleOrder(models.Model):
_inherit = 'sale.order'
def action_apply_volume_discount(self):
for order in self:
total_qty = sum(order.order_line.mapped('product_uom_qty'))
if total_qty > VOLUME_THRESHOLD:
order.order_line.write({{'discount': VOLUME_DISCOUNT}})
Ejecutas el test → pasa. 🎉
🔴 Paso 3 — Segundo test (borde)
def test_no_discount_below_threshold(self):
"""Orders with qty <= 10 keep 0% discount."""
order = self.env['sale.order'].create({{
'partner_id': self.partner.id,
'order_line': [(0, 0, {{
'product_id': self.product.id,
'product_uom_qty': 5,
}})],
}})
order.action_apply_volume_discount()
self.assertEqual(order.order_line[0].discount, 0.0)
Probablemente ya pasa con el código actual. Si no, ajustas hasta que pase.
🔵 Paso 4 — Refactor
def _get_total_quantity(self):
self.ensure_one()
return sum(self.order_line.mapped('product_uom_qty'))
def action_apply_volume_discount(self):
for order in self:
if order._get_total_quantity() > VOLUME_THRESHOLD:
order.order_line.write({{'discount': VOLUME_DISCOUNT}})
Ejecutas todos los tests → siguen pasando. Código más legible, mismo comportamiento.
Framework
Herramientas de testing en Odoo 19
| Clase base | Cuándo usarla | Velocidad |
|---|---|---|
TransactionCase | Tests de modelos, ORM, workflows | Media (rollback al final) |
SavepointCase | Tests aislados con savepoints | Rápida |
HttpCase | Controladores, rutas web, portal | Lenta (HTTP real) |
Tagged('post_install') | Tests que necesitan módulos instalados | — |
# Ejecutar tests de un módulo
docker compose exec odoo \
/opt/odoo/venv/bin/python /opt/odoo/odoo/odoo-bin \
-c /opt/odoo/odoo.conf \
-d odoo \
--test-enable \
--stop-after-init \
-i mi_modulo \
--log-level=test
Buenas prácticas
Reglas para tests Odoo efectivos
- Nombres descriptivos:
test_confirm_sale_order_updates_state_to_sale - Un assert por concepto: no mezcles validación de precio y de estado
- setUpClass para datos compartidos: no recrees productos en cada test
- No dependas de IDs fijos: usa
self.env.ref()o crea registros - Mockea integraciones externas: APIs, SMTP, pasarelas de pago
- Tests atómicos: cada test debe poder ejecutarse solo
- Cobertura mínima 80% en código nuevo; lógica crítica ~100%
Qué testear
Prioridades en módulos Odoo
| Capa | Qué testear | Ejemplo |
|---|---|---|
| Modelos | create, write, computes, constraints | test_age_computed_from_birth_date |
| Workflows | Transiciones de estado, botones de acción | test_confirm_creates_diagnostic_test |
| Seguridad | ACL, record rules, acceso por grupo | test_user_cannot_read_other_partner_tests |
| Wizards | Flujos multi-paso, valores por defecto | test_wizard_assigns_professional |
| Cron | Jobs programados, idempotencia | test_birthday_cron_sends_once_per_year |
| Controladores | Rutas HTTP, permisos, respuestas | HttpCase con self.url_open() |
Integración
TDD en el flujo de trabajo diario
- Recibes la tarea / historia de usuario
- Escribes el test que describe el criterio de aceptación
- Verificas que falla (Red)
- Implementas el mínimo (Green)
- Refactorizas si hace falta (Refactor)
- Commit con mensaje convencional:
test:ofeat: - CI ejecuta tests antes del merge
⚠️ Trampa común
Resumen
TDD en Odoo no requiere herramientas exóticas: TransactionCase, el ORM y disciplina. Escribe el test primero, implementa lo mínimo, refactoriza con confianza. Combinado con SOLID, obtienes módulos que se pueden evolucionar sin miedo — y eso, en un ERP que vive años, no tiene precio.