Saltar al contenido principal
Odoo · Postmortem de ingeniería

Los tests estaban en verde y el conector no funcionaba: 32 fallos que la suite no veía

Una suite en verde demuestra que tu código se comporta como tu mock. No demuestra que la API remota siga siendo la que creías. Aquí está el caso, con números, y las pruebas que sí lo habrían detectado.

Pantalla de Apps de Odoo con los módulos instalados y activos en un backend real

Si tus tests de Odoo pasan y el conector falla en producción, el motivo casi siempre es el mismo: la suite no prueba la API, prueba tu mock de la API. El mock lo escribiste tú, contra la documentación que había aquel día, y desde entonces no ha cambiado. La API sí. Un .get("shippingDate") cuyo campo ahora se llama de otra forma no lanza ninguna excepción: devuelve None para siempre, y el test que lo cubre sigue en verde porque el mock también dice shippingDate.

Idea clave: un test contra un mock demuestra coherencia interna, no conformidad externa. Solo una de las dos te avisa cuando el otro lado cambia.

El caso: 236 de 236 en verde en Odoo 19, 18 y 17

Auditamos un conector de marketplace nuestro antes de ponerlo delante de un vendedor real. La suite daba 236 de 236 en verde, y no en local: sobre builds reales de Odoo.sh en las tres series que soportamos.

No funcionaba. La auditoría encontró 32 fallos reales por debajo del verde: 20 de contrato y 12 de versión. Ninguno de los 236 tests los tocaba, porque ninguno miraba fuera del módulo.

Qué se había roto de verdad: 20 fallos de contrato y 12 de versión

Un fallo de contrato es una discrepancia entre lo que el módulo envía o lee y lo que la API declara. Salieron tres sabores:

  • Un parámetro de consulta requerido que no se enviaba. La API responde 400 y el fallo es ruidoso… en producción.
  • Un parámetro que la especificación no declara en ninguna operación. Eso no da error: se ignora. La respuesta llega sin filtrar y parece que funcionó. Este es el peligroso.
  • Un campo de respuesta leído con el nombre equivocado. .get() devuelve None y nada revienta, nunca.

Cuatro de esos veinte no salieron leyendo la especificación: salieron llamando. La cabecera Link de paginación llegaba relativa, y tal cual no era una URL válida: a partir de la página uno no se importaba ni un pedido. Y un 403 por superar la cuota no venía en el formato de error documentado, así que se leía como un fallo de permisos: el conector marcaba la cuenta como no autorizada y detenía toda la sincronización.

Un fallo de versión es otra cosa: el mismo código se comporta distinto según la serie de Odoo. Estos salieron del propio módulo:

  • models.Constraint(...) existe en Odoo 19. En 18 y 17 no es una restricción: la clase carga, no hay ningún error, y la garantía de unicidad ha desaparecido.
  • res.groups deletrea sus miembros users antes de Odoo 19 y user_ids a partir de 19. Si te equivocas de lado, el módulo no instala.
  • Un ir.cron sin numbercall en Odoo 17 toma el valor por defecto 1: el cron corre una vez y se desactiva solo. Nadie ve un error. Simplemente deja de sincronizar el martes.
  • Odoo 17 escribe <tree> donde 18 y 19 escriben <list>, y rechaza una vista cuyo invisible lee un campo que la vista no pinta: 18 y 19 lo toleran.

Fíjate en el patrón: la mitad de esa lista no da error. Da silencio. Y el silencio no lo caza ninguna aserción que no lo busque.

Por qué la suite no lo vio: la anatomía de un test que se prueba a sí mismo

Un test típico de conector tiene tres piezas: un mock que devuelve un JSON, la llamada al método y una aserción sobre el resultado. Si ese JSON lo escribiste mirando tu propio parser en lugar de la especificación del proveedor, el test es un espejo: pasa siempre, y el día que la API cambie también.

Hay una segunda forma de verde vacío, y es específica de Odoo: la suite no llegó a ejecutarse. En el runner de Odoo.sh los tests post_install solo corren en una instalación real: un -u, o un -i sobre un módulo ya instalado, ejecuta cero tests y sale con código 0. Y los selectores --test-tags "/modulo:Clase.metodo" no casan con nada ahí; solo funciona /modulo. Un pipeline mal escrito informa «sin fallos» porque no hubo tests.

Lo aprendimos con un dato incómodo y nuestro: uno de nuestros propios verificadores de conformidad informaba cero hallazgos por un bug suyo. Resolvía solo components/schemas, cuando las respuestas 200 de aquella API apuntan a components/responses, así que se saltaba casi todos los endpoints. «Cero hallazgos» y «cero endpoints revisados» se escriben igual.

El segundo caso, peor: un doble reembolso con la suite en verde

Segunda auditoría, otro conector, de una plataforma de marketplaces. 163 de 163 en verde, también en las tres series y sobre builds reales. Por debajo: el registro de llamadas no había escrito nunca una fila, la sincronización incremental pedía la ventana de fechas equivocada, y un reembolso se podía enviar dos veces.

El de dinero es el que un mock no puede reproducir. El cliente HTTP reintentaba ante 5xx y ante timeout, y para cualquier método: también para el PUT que envía un reembolso. Un timeout no dice que la petición no llegara, dice que no sabes si llegó; reintentar ahí una escritura no idempotente es reembolsar dos veces. Y las redirecciones se seguían reescribiendo el método: un PUT de reembolso podía acabar en un GET sin credenciales que devolvía 200 y se anotaba como éxito. Ningún test lo vio, y no fue descuido: un mock contesta a la primera, siempre, sin timeouts, sin 502 y sin redirecciones. La rama que hace daño es la que el mock nunca ejecuta.

El de las fechas cabe en una palabra: las sincronizaciones incrementales pedían lo creado desde la última pasada en vez de lo cambiado. Un pedido creado por la mañana y enviado por la tarde no volvía a entrar en la ventana, y su estado en Odoo se quedaba congelado.

El del log es el más silencioso de los tres. El ayudante que devuelve el modelo de registro devuelve un recordset, y cuando todo va bien ese recordset está vacío; en Odoo, un recordset vacío es falso. La comprobación era if not log_model: return False, así que acertaba siempre y todos los caminos de «esto lo registramos» eran un no-op: ni excepción, ni traza, ni fila. La prueba de que llevaba así desde el primer día no salió de ningún test, sino de mirar en producción el contador de la secuencia de esa tabla y encontrarlo en NULL después de meses de crones.

Los tres tipos de prueba que sí lo habrían pillado

1. Contrato contra la especificación real, no contra tu mock

No hace falta ningún broker ni infraestructura: basta con tener el OpenAPI del proveedor en el repositorio y compararlo, estáticamente, contra tu cliente. Tres puertas que corren en segundos y salen con código 1 si encuentran algo:

  • Parámetros de consulta: qué declara cada operación frente a lo que envía cada wrapper. Caza el requerido que falta y el nombre que no declara nadie.
  • Cuerpos de escritura: las claves de primer nivel del payload contra el esquema de la petición.
  • Parsers de respuesta: cada nombre de campo que lee el código contra el esquema de la respuesta.

La corrida sobre ese conector: 87 wrappers mapeados contra 88 operaciones de la especificación, 20 rutas de escritura con cuerpo JSON, 38 parsers sobre 38 operaciones distintas (5 funciones multi-endpoint no analizables), 0 hallazgos. Ese cero significa algo porque lleva el denominador impreso al lado.

Y cuando el proveedor da sandbox, una llamada real por familia de endpoint: es la única prueba que distingue «la especificación dice esto» de «el servidor hace esto».

2. Instalación real, no import (y después, ejercitar)

Que un módulo importe no dice nada; que compile, tampoco. La prueba es instalarlo: -i sobre una base limpia y -i sobre una base que ya tenía la versión anterior. Son dos pruebas distintas, y la segunda es la que revienta con los datos noupdate y las migraciones. Lo desarrollamos en un módulo que compila en Odoo 19 y aun así no se instala.

Y después de instalar, ejercitarlo. Pasamos 47 apps de nuestro catálogo por instalaciones limpias de Odoo 17 y 18 Community —instalar y luego llamar a get_views, search_read y onchange—: 61 de 94 corridas de versión salieron limpias. De las 33 restantes, la mayoría ni llegaba a instalarse, cuatro eran artefactos de probar cada app sin su módulo hermano, y dos instalaban perfectamente y reventaban al ejercitarlas: un Expected singleton en un compute de product.template y un modelo que asumía Enterprise en una base Community.

3. Verificación en las tres versiones, una a una

Un módulo que va a 19, 18 y 17 son tres módulos que se parecen. La lista de trampas de más arriba la tenemos como un chequeo estático que corre en un segundo sobre el port generado, antes de gastar un build: cada entrada fue un fallo real y la mitad no fallaban ruidosamente. Es también el fondo del argumento cuando alguien compara conector nativo contra capa intermedia: la superficie de contrato no desaparece, solo cambia de dueño. Lo tratamos en Odoo 19 frente a los conectores de middleware.

Qué cambiar el lunes

  • Guarda la especificación del proveedor en el repositorio, versionada junto al código que la consume. Su diff es tu alarma.
  • Ponle fecha y versión de contrato a cada fixture. Un mock que no dice contra qué se escribió ya ha caducado y no lo sabes.
  • Escribe un comparador estático entre tu cliente y esa especificación. Doscientas líneas de Python que salgan con código distinto de cero.
  • Haz que el pipeline falle si el número de tests ejecutados es cero, o si baja respecto a la corrida anterior. «0 fallos» no significa nada; «0 fallos de 236», sí.
  • Instala de verdad en la CI —base limpia y base con la versión anterior— y ejercita después: abre las vistas y lee los registros por ORM. Y si mantienes varias series de Odoo, corre la suite en todas.

Y el cambio que de verdad cuesta, que no es técnico: «la suite está verde» no responde a «¿funciona?», responde a «¿mi código sigue haciendo lo que yo creía?». Son dos preguntas, y confundirlas nos costó 32 fallos en un conector y un doble reembolso latente en otro.

Si mantienes conectores para clientes y quieres una segunda lectura sobre lo que tu suite no está probando, así trabajamos con consultorías y equipos técnicos y con otros partners de Odoo.

Enlaces útiles dentro de FlexigoTech

Trabajo con partners de OdooCómo nos integramos con equipos que ya tienen su propio ciclo de desarrolloIngeniería de Odoo para consultoríasCapacidad técnica para consultorías que necesitan conectores mantenidosOdoo 19 frente a los conectores de middlewareDónde vive la superficie de contrato cuando metes una capa intermediaConector de Mirakl para OdooCómo abordamos la sincronización de pedidos, devoluciones y liquidaciones

Preguntas frecuentes

¿Por qué mis tests de Odoo pasan y el conector falla en producción?

Porque la suite prueba tu mock, no la API. El mock lo escribiste contra la documentación de aquel día y no ha cambiado desde entonces; la API sí. Un campo leído con el nombre equivocado devuelve None sin lanzar excepción, así que ninguna aserción se entera. La única forma de detectarlo es comparar tu cliente contra la especificación oficial del proveedor, o hacer al menos una llamada real al sandbox.

¿Qué es contract testing aplicado a un conector de Odoo?

En su versión práctica, no necesita broker ni infraestructura: consiste en guardar el OpenAPI del proveedor en el repositorio y comparar estáticamente contra él los parámetros que envías, las claves de los cuerpos de escritura y los nombres de campo que lee cada parser. Tres scripts que salen con código distinto de cero. Cubren la mayoría de fallos de contrato antes de arrancar Odoo.

¿Cómo sé si mi mock está desfasado respecto a la API?

Tres señales: el fixture no indica contra qué versión de la especificación se escribió; la especificación del proveedor no está versionada en el repositorio, así que no puedes ver su diff; y ningún test toca la red nunca. Si se cumplen las tres, tu mock ya está desfasado aunque la suite esté verde.

¿Basta con probar el módulo en Odoo 19 si también lo publico en 18 y 17?

No. Hay construcciones que existen en 19 y en 18 o 17 no fallan: se ignoran. models.Constraint en 18 o 17 no es una restricción y no da error, así que pierdes la unicidad en silencio. Un ir.cron sin numbercall en 17 se apaga solo tras la primera ejecución. Al pasar 47 apps por instalaciones limpias de 17 y 18 Community, 61 de 94 corridas salieron limpias: un tercio no.

¿Un test de instalación es lo mismo que un test de import?

No, y confundirlos es de los errores más caros. Importar el módulo solo prueba que el Python es válido. Instalarlo carga el XML, valida las vistas, aplica la seguridad y ejecuta las migraciones. Y aun instalado, hay que ejercitarlo: en nuestro propio catálogo hubo apps que instalaban perfectamente y reventaban al abrir una vista o leer un registro por ORM.

¿Tienes un conector con la suite en verde y dudas de que funcione?

Revisamos la superficie de contrato contra la especificación real del proveedor y la instalación en las series de Odoo que soportas. Escribe a comercial@flexigobe.com o llama al +34 616 809 504.

Hablar con un ingeniero