Si els teus tests d'Odoo passen i el connector falla en producció, el motiu gairebé sempre és el mateix: la suite no prova l'API, prova el teu mock de l'API. El mock el vas escriure tu, contra la documentació que hi havia aquell dia, i des d'aleshores no ha canviat. L'API sí. Un .get("shippingDate") el camp del qual ara es diu d'una altra manera no llança cap excepció: retorna None per sempre, i el test que el cobreix continua en verd perquè el mock també diu shippingDate.
Idea clau: un test contra un mock demostra coherència interna, no conformitat externa. Només una de les dues t'avisa quan l'altre costat canvia.
El cas: 236 de 236 en verd a Odoo 19, 18 i 17
Vam auditar un connector de marketplace nostre abans de posar-lo davant d'un venedor real. La suite donava 236 de 236 en verd, i no en local: sobre builds reals d'Odoo.sh a les tres sèries que donem suport.
No funcionava. L'auditoria va trobar 32 errors reals per sota del verd: 20 de contracte i 12 de versió. Cap dels 236 tests els tocava, perquè cap mirava fora del mòdul.
Què s'havia trencat de veritat: 20 errors de contracte i 12 de versió
Un error de contracte és una discrepància entre el que el mòdul envia o llegeix i el que l'API declara. Van sortir tres sabors:
- Un paràmetre de consulta requerit que no s'enviava. L'API respon 400 i l'error és sorollós… en producció.
- Un paràmetre que l'especificació no declara en cap operació. Això no dona error: s'ignora. La resposta arriba sense filtrar i sembla que ha funcionat. Aquest és el perillós.
- Un camp de resposta llegit amb el nom equivocat.
.get()retornaNonei no rebenta res, mai.
Quatre d'aquests vint no van sortir llegint l'especificació: van sortir cridant. La capçalera Link de paginació arribava relativa, i tal qual no era una URL vàlida: a partir de la pàgina u no s'importava ni una comanda. I un 403 per superar la quota no venia en el format d'error documentat, així que es llegia com un error de permisos: el connector marcava el compte com a no autoritzat i aturava tota la sincronització.
Un error de versió és una altra cosa: el mateix codi es comporta diferent segons la sèrie d'Odoo. Aquests van sortir del propi mòdul:
models.Constraint(...)existeix a Odoo 19. A 18 i 17 no és una restricció: la classe carrega, no hi ha cap error, i la garantia d'unicitat ha desaparegut.res.groupsescriu els seus membresusersabans d'Odoo 19 iuser_idsa partir de 19. Si t'equivoques de costat, el mòdul no s'instal·la.- Un
ir.cronsensenumbercalla Odoo 17 pren el valor per defecte 1: el cron s'executa una vegada i es desactiva sol. Ningú veu cap error. Simplement deixa de sincronitzar el dimarts. - Odoo 17 escriu
<tree>on 18 i 19 escriuen<list>, i rebutja una vista elinvisiblede la qual llegeix un camp que la vista no pinta: 18 i 19 ho toleren.
Fixa't en el patró: la meitat d'aquesta llista no dona error. Dona silenci. I el silenci no el caça cap asserció que no el busqui.
Per què la suite no ho va veure: l'anatomia d'un test que es prova a si mateix
Un test típic de connector té tres peces: un mock que retorna un JSON, la crida al mètode i una asserció sobre el resultat. Si aquest JSON el vas escriure mirant el teu propi parser en lloc de l'especificació del proveïdor, el test és un mirall: passa sempre, i el dia que l'API canviï també.
Hi ha una segona forma de verd buit, i és específica d'Odoo: la suite no va arribar a executar-se. Al runner d'Odoo.sh els tests post_install només corren en una instal·lació real: un -u, o un -i sobre un mòdul ja instal·lat, executa zero tests i surt amb codi 0. I els selectors --test-tags "/modulo:Clase.metodo" no encaixen amb res allà; només funciona /modulo. Un pipeline mal escrit informa «sense errors» perquè no hi va haver tests.
Ho vam aprendre amb una dada incòmoda i nostra: un dels nostres propis verificadors de conformitat informava zero troballes per un bug seu. Resolia només components/schemas, quan les respostes 200 d'aquella API apunten a components/responses, així que se saltava gairebé tots els endpoints. «Zero troballes» i «zero endpoints revisats» s'escriuen igual.
El segon cas, pitjor: un doble abonament amb la suite en verd
Segona auditoria, un altre connector, d'una plataforma de marketplaces. 163 de 163 en verd, també a les tres sèries i sobre builds reals. Per sota: el registre de crides no havia escrit mai cap fila, la sincronització incremental demanava la finestra de dates equivocada, i un abonament es podia enviar dues vegades.
El dels diners és el que un mock no pot reproduir. El client HTTP reintentava davant de 5xx i davant de timeout, i per a qualsevol mètode: també per al PUT que envia un abonament. Un timeout no diu que la petició no arribés, diu que no saps si va arribar; reintentar-hi una escriptura no idempotent és abonar dues vegades. I les redireccions se seguien reescrivint el mètode: un PUT d'abonament podia acabar en un GET sense credencials que retornava 200 i s'anotava com a èxit. Cap test ho va veure, i no va ser un descuit: un mock respon a la primera, sempre, sense timeouts, sense 502 i sense redireccions. La branca que fa mal és la que el mock mai no executa.
El de les dates cap en una paraula: les sincronitzacions incrementals demanaven el que s'havia creat des de l'última passada en lloc del que havia canviat. Una comanda creada al matí i enviada a la tarda no tornava a entrar a la finestra, i el seu estat a Odoo es quedava congelat.
El del log és el més silenciós dels tres. L'ajudant que retorna el model de registre retorna un recordset, i quan tot va bé aquest recordset és buit; a Odoo, un recordset buit és fals. La comprovació era if not log_model: return False, així que encertava sempre i tots els camins de «això ho registrem» eren un no-op: ni excepció, ni traça, ni fila. La prova que feia així des del primer dia no va sortir de cap test, sinó de mirar en producció el comptador de la seqüència d'aquesta taula i trobar-lo a NULL després de mesos de crons.
Els tres tipus de prova que sí ho haurien pillat
1. Contracte contra l'especificació real, no contra el teu mock
No cal cap broker ni infraestructura: n'hi ha prou amb tenir l'OpenAPI del proveïdor al repositori i comparar-lo, estàticament, contra el teu client. Tres portes que corren en segons i surten amb codi 1 si troben alguna cosa:
- Paràmetres de consulta: què declara cada operació davant del que envia cada wrapper. Caça el requerit que falta i el nom que no declara ningú.
- Cossos d'escriptura: les claus de primer nivell del payload contra l'esquema de la petició.
- Parsers de resposta: cada nom de camp que llegeix el codi contra l'esquema de la resposta.
La corrida sobre aquest connector: 87 wrappers mapats contra 88 operacions de l'especificació, 20 rutes d'escriptura amb cos JSON, 38 parsers sobre 38 operacions diferents (5 funcions multi-endpoint no analitzables), 0 troballes. Aquest zero significa alguna cosa perquè porta el denominador imprès al costat.
I quan el proveïdor dona sandbox, una crida real per família d'endpoint: és l'única prova que distingeix «l'especificació diu això» de «el servidor fa això».
2. Instal·lació real, no import (i després, exercitar)
Que un mòdul importi no diu res; que compili, tampoc. La prova és instal·lar-lo: -i sobre una base neta i -i sobre una base que ja tenia la versió anterior. Són dues proves diferents, i la segona és la que rebenta amb les dades noupdate i les migracions. Ho vam desenvolupar a un mòdul que compila a Odoo 19 i tot i així no s'instal·la.
I després d'instal·lar, exercitar-lo. Vam passar 47 apps del nostre catàleg per instal·lacions netes d'Odoo 17 i 18 Community (instal·lar i després cridar get_views, search_read i onchange): 61 de 94 corregudes de versió van sortir netes. De les 33 restants, la majoria ni arribava a instal·lar-se, quatre eren artefactes de provar cada app sense el seu mòdul germà, i dues s'instal·laven perfectament i rebentaven en exercitar-les: un Expected singleton en un compute de product.template i un model que assumia Enterprise en una base Community.
3. Verificació a les tres versions, una a una
Un mòdul que va a 19, 18 i 17 són tres mòduls que s'assemblen. La llista de paranys de més amunt la tenim com un xequeig estàtic que corre en un segon sobre el port generat, abans de gastar un build: cada entrada va ser un error real i la meitat no fallaven sorollosament. És també el fons de l'argument quan algú compara connector natiu contra capa intermèdia: la superfície de contracte no desapareix, només canvia de propietari. Ho tractem a Odoo 19 davant dels connectors de middleware.
Què canviar dilluns
- Guarda l'especificació del proveïdor al repositori, versionada juntament amb el codi que la consumeix. El seu diff és la teva alarma.
- Posa-li data i versió de contracte a cada fixture. Un mock que no diu contra què es va escriure ja ha caducat i no ho saps.
- Escriu un comparador estàtic entre el teu client i aquesta especificació. Dues-centes línies de Python que surtin amb codi diferent de zero.
- Fes que el pipeline falli si el nombre de tests executats és zero, o si baixa respecte a la corrida anterior. «0 errors» no vol dir res; «0 errors de 236», sí.
- Instal·la de veritat a la CI (base neta i base amb la versió anterior) i exercita després: obre les vistes i llegeix els registres per ORM. I si mantens diverses sèries d'Odoo, corre la suite a totes.
I el canvi que de veritat costa, que no és tècnic: «la suite està verda» no respon a «funciona?», respon a «el meu codi continua fent el que jo creia?». Són dues preguntes, i confondre-les ens va costar 32 errors en un connector i un doble abonament latent en un altre.
Si mantens connectors per a clients i vols una segona lectura sobre el que la teva suite no està provant, així treballem amb consultories i equips tècnics i amb altres partners d'Odoo.
Enllaços útils dins de FlexigoTech
Preguntes freqüents
Per què els meus tests d'Odoo passen i el connector falla en producció?
Perquè la suite prova el teu mock, no l'API. El mock el vas escriure contra la documentació d'aquell dia i no ha canviat des d'aleshores; l'API sí. Un camp llegit amb el nom equivocat retorna None sense llançar excepció, així que cap asserció se n'assabenta. L'única manera de detectar-ho és comparar el teu client contra l'especificació del proveïdor, o fer almenys una crida real al sandbox.
Què és contract testing aplicat a un connector d'Odoo?
En la seva versió pràctica, no necessita broker ni infraestructura: consisteix a guardar l'OpenAPI del proveïdor al repositori i comparar-hi estàticament els paràmetres que envies, les claus dels cossos d'escriptura i els noms de camp que llegeix cada parser. Tres scripts que surten amb codi diferent de zero. Cobreixen la majoria d'errors de contracte abans d'arrencar Odoo.
Com sé si el meu mock està desfasat respecte a l'API?
Tres senyals: el fixture no indica contra quina versió de l'especificació es va escriure; l'especificació del proveïdor no està versionada al repositori, així que no en pots veure el diff; i cap test toca mai la xarxa. Si es compleixen les tres, el teu mock ja està desfasat encara que la suite estigui en verd.
N'hi ha prou amb provar el mòdul a Odoo 19 si també el publico a 18 i 17?
No. Hi ha construccions que existeixen a 19 i a 18 o 17 no fallen: s'ignoren. models.Constraint a 18 o 17 no és una restricció i no dona error, així que perds la unicitat en silenci. Un ir.cron sense numbercall a 17 s'apaga sol després de la primera execució. En passar 47 apps per instal·lacions netes de 17 i 18 Community, 61 de 94 corregudes van sortir netes: un terç no.
Un test d'instal·lació és el mateix que un test d'import?
No, i confondre'ls és dels errors més cars. Importar el mòdul només prova que el Python és vàlid. Instal·lar-lo carrega el XML, valida les vistes, aplica la seguretat i executa les migracions. I fins i tot instal·lat, cal exercitar-lo: al nostre propi catàleg hi va haver apps que s'instal·laven perfectament i rebentaven en obrir una vista o llegir un registre per ORM.
El que fem sobre això
Tens un connector amb la suite en verd i dubtes que funcioni?
Revisem la superfície de contracte contra l'especificació real del proveïdor i la instal·lació a les sèries d'Odoo que suportes. Escriu a comercial@flexigobe.com o truca al +34 616 809 504.

