Aller au contenu principal
Odoo · Postmortem d'ingénierie

Les tests étaient verts et le connecteur ne fonctionnait pas : 32 défauts que la suite ne voyait pas

Une suite verte prouve que votre code se comporte comme votre mock. Elle ne prouve pas que l'API distante est toujours celle que vous croyiez. Voici le cas, chiffres à l'appui, et les tests qui l'auraient détecté.

Écran Apps d'Odoo avec les modules installés et actifs sur un backend réel

Si vos tests Odoo passent et que le connecteur échoue en production, la raison est presque toujours la même : la suite ne teste pas l'API, elle teste votre mock de l'API. Le mock, c'est vous qui l'avez écrit, d'après la documentation du jour, et il n'a pas bougé depuis. L'API, si. Un .get("shippingDate") dont le champ porte désormais un autre nom ne lève aucune exception : il renvoie None indéfiniment, et le test qui le couvre reste vert parce que le mock dit aussi shippingDate.

Idée clé : un test contre un mock démontre la cohérence interne, pas la conformité externe. Une seule des deux vous prévient quand l'autre côté change.

Le cas : 236 tests sur 236 au vert sur Odoo 19, 18 et 17

Nous avons audité l'un de nos connecteurs de marketplace avant de le mettre entre les mains d'un vrai vendeur. La suite affichait 236 sur 236 au vert, et pas en local : sur de vrais builds Odoo.sh dans les trois séries que nous prenons en charge.

Il ne fonctionnait pas. L'audit a trouvé 32 défauts réels sous le vert : 20 de contrat et 12 de version. Aucun des 236 tests n'y touchait, car aucun ne regardait en dehors du module.

Ce qui était vraiment cassé : 20 défauts de contrat et 12 de version

Un défaut de contrat est un écart entre ce que le module envoie ou lit et ce que l'API déclare. Il est apparu sous trois formes :

  • Un paramètre de requête obligatoire qui n'était pas envoyé. L'API répond 400 et l'échec est bruyant… en production.
  • Un paramètre que la spécification ne déclare dans aucune opération. Cela ne produit pas d'erreur : il est ignoré. La réponse arrive non filtrée et on croit que cela a marché. C'est le plus dangereux.
  • Un champ de réponse lu sous le mauvais nom. .get() renvoie None et rien ne plante, jamais.

Quatre de ces vingt défauts ne sont pas sortis de la lecture de la spécification : ils sont sortis des appels. L'en-tête de pagination Link arrivait relatif, et tel quel ce n'était pas une URL valide : dès la page un, plus aucune commande n'était importée. Et un 403 pour dépassement de quota n'arrivait pas dans le format d'erreur documenté, donc il était lu comme un échec de permissions : le connecteur marquait le compte comme non autorisé et arrêtait toute la synchronisation.

Un défaut de version, c'est autre chose : le même code se comporte différemment selon la série d'Odoo. Ceux-ci viennent du module lui-même :

  • models.Constraint(...) existe dans Odoo 19. Dans 18 et 17, ce n'est pas une contrainte : la classe se charge, aucune erreur, et la garantie d'unicité a disparu.
  • res.groups écrit ses membres users avant Odoo 19 et user_ids à partir de 19. Si vous vous trompez de côté, le module ne s'installe pas.
  • Un ir.cron sans numbercall dans Odoo 17 prend la valeur par défaut 1 : le cron s'exécute une fois puis se désactive tout seul. Personne ne voit d'erreur. Il cesse simplement de synchroniser le mardi.
  • Odoo 17 écrit <tree> là où 18 et 19 écrivent <list>, et rejette une vue dont le invisible lit un champ que la vue n'affiche pas : 18 et 19 le tolèrent.

Remarquez le schéma : la moitié de cette liste ne produit pas d'erreur. Elle produit du silence. Et le silence, aucune assertion ne le détecte si elle ne le cherche pas.

Pourquoi la suite ne l'a pas vu : l'anatomie d'un test qui se teste lui-même

Un test de connecteur typique a trois pièces : un mock qui renvoie un JSON, l'appel à la méthode et une assertion sur le résultat. Si ce JSON, vous l'avez écrit en regardant votre propre parseur plutôt que la spécification du fournisseur, le test est un miroir : il passe toujours, et le jour où l'API change, il passe encore.

Il existe une seconde forme de vert vide, propre à Odoo : la suite ne s'est jamais exécutée. Dans le runner d'Odoo.sh, les tests post_install ne tournent que sur une installation réelle : un -u, ou un -i sur un module déjà installé, exécute zéro test et sort avec le code 0. Et les sélecteurs --test-tags "/modulo:Clase.metodo" ne correspondent à rien là-bas ; seul /modulo fonctionne. Un pipeline mal écrit annonce « aucun échec » parce qu'il n'y a pas eu de tests.

Nous l'avons appris avec une donnée gênante, et la nôtre : l'un de nos propres vérificateurs de conformité signalait zéro constat à cause d'un bug à lui. Il ne résolvait que components/schemas, alors que les réponses 200 de cette API pointent vers components/responses, si bien qu'il sautait presque tous les endpoints. « Zéro constat » et « zéro endpoint examiné » s'écrivent pareil.

Le second cas, pire : un double remboursement avec la suite au vert

Seconde audit, autre connecteur, d'une plateforme de marketplaces. 163 sur 163 au vert, là aussi dans les trois séries et sur de vrais builds. En dessous : le journal des appels n'avait jamais écrit une seule ligne, la synchronisation incrémentale demandait la mauvaise fenêtre de dates, et un remboursement pouvait être envoyé deux fois.

Celui qui touche à l'argent est celui qu'un mock ne peut pas reproduire. Le client HTTP réessayait sur les 5xx et sur timeout, et pour n'importe quelle méthode : y compris pour le PUT qui envoie un remboursement. Un timeout ne dit pas que la requête n'est pas arrivée, il dit que vous ne savez pas si elle est arrivée ; réessayer là une écriture non idempotente, c'est rembourser deux fois. Et les redirections étaient suivies en réécrivant la méthode : un PUT de remboursement pouvait finir en GET sans identifiants, qui renvoyait 200 et était enregistré comme un succès. Aucun test ne l'a vu, et ce n'est pas de la négligence : un mock répond du premier coup, toujours, sans timeouts, sans 502 et sans redirections. La branche qui fait mal est celle que le mock n'exécute jamais.

Celui des dates tient en un mot : les synchronisations incrémentales demandaient ce qui avait été créé depuis le dernier passage au lieu de ce qui avait changé. Une commande créée le matin et expédiée l'après-midi ne retombait plus dans la fenêtre, et son statut dans Odoo restait figé.

Celui du journal est le plus silencieux des trois. L'assistant qui renvoie le modèle de journalisation renvoie un recordset, et quand tout va bien ce recordset est vide ; dans Odoo, un recordset vide est faux. La vérification était if not log_model: return False, donc elle réussissait toujours et tous les chemins « cela, nous le journalisons » étaient un no-op : ni exception, ni trace, ni ligne. La preuve que c'était ainsi depuis le premier jour ne vient d'aucun test, mais d'avoir regardé en production le compteur de la séquence de cette table et de l'avoir trouvé à NULL après des mois de crons.

Les trois types de test qui l'auraient détecté

1. Contrat contre la spécification réelle, pas contre votre mock

Pas besoin de broker ni d'infrastructure : il suffit d'avoir l'OpenAPI du fournisseur dans le dépôt et de le comparer, statiquement, à votre client. Trois garde-fous qui tournent en quelques secondes et sortent avec le code 1 s'ils trouvent quelque chose :

  • Paramètres de requête : ce que déclare chaque opération face à ce qu'envoie chaque wrapper. Il attrape l'obligatoire manquant et le nom que personne ne déclare.
  • Corps d'écriture : les clés de premier niveau du payload contre le schéma de la requête.
  • Parseurs de réponse : chaque nom de champ lu par le code contre le schéma de la réponse.

L'exécution sur ce connecteur : 87 wrappers mappés sur 88 opérations de la spécification, 20 routes d'écriture avec corps JSON, 38 parseurs sur 38 opérations distinctes (5 fonctions multi-endpoints non analysables), 0 constat. Ce zéro a un sens parce qu'il porte son dénominateur imprimé à côté.

Et quand le fournisseur propose un sandbox, un appel réel par famille d'endpoint : c'est le seul test qui distingue « la spécification dit ceci » de « le serveur fait cela ».

2. Installation réelle, pas import (puis exercer)

Qu'un module s'importe ne dit rien ; qu'il compile non plus. Le test, c'est de l'installer : -i sur une base propre et -i sur une base qui avait déjà la version précédente. Ce sont deux tests différents, et le second est celui qui explose avec les données noupdate et les migrations. Nous l'avons détaillé dans un module qui compile sur Odoo 19 et ne s'installe pourtant pas.

Et après l'installation, l'exercer. Nous avons passé 47 apps de notre catalogue sur des installations propres d'Odoo 17 et 18 Community (installer puis appeler get_views, search_read et onchange) : 61 des 94 exécutions par version sont sorties propres. Sur les 33 restantes, la plupart ne s'installaient même pas, quatre étaient des artefacts dus au test de chaque app sans son module frère, et deux s'installaient parfaitement et plantaient à l'exercice : un Expected singleton dans un compute de product.template et un modèle qui supposait Enterprise sur une base Community.

3. Vérification dans les trois versions, une par une

Un module qui tourne sur 19, 18 et 17, ce sont trois modules qui se ressemblent. La liste des pièges ci-dessus, nous l'avons sous forme de contrôle statique qui s'exécute en une seconde sur le port généré, avant de dépenser un build : chaque entrée était un vrai défaut et la moitié ne plantaient pas bruyamment. C'est aussi le fond de l'argument quand on compare connecteur natif et couche intermédiaire : la surface de contrat ne disparaît pas, elle change seulement de propriétaire. Nous le traitons dans Odoo 19 face aux connecteurs de middleware.

Quoi changer lundi

  • Gardez la spécification du fournisseur dans le dépôt, versionnée avec le code qui la consomme. Son diff est votre alarme.
  • Donnez une date et une version de contrat à chaque fixture. Un mock qui ne dit pas contre quoi il a été écrit a déjà expiré et vous ne le savez pas.
  • Écrivez un comparateur statique entre votre client et cette spécification. Deux cents lignes de Python qui sortent avec un code différent de zéro.
  • Faites échouer le pipeline si le nombre de tests exécutés est zéro, ou s'il baisse par rapport à l'exécution précédente. « 0 échec » ne veut rien dire ; « 0 échec sur 236 », si.
  • Installez pour de vrai en CI (base propre et base avec la version précédente) puis exercez : ouvrez les vues et lisez les enregistrements par l'ORM. Et si vous maintenez plusieurs séries d'Odoo, lancez la suite sur toutes.

Et le changement qui coûte vraiment, qui n'est pas technique : « la suite est verte » ne répond pas à « est-ce que ça marche ? », elle répond à « mon code fait-il toujours ce que je croyais ? ». Ce sont deux questions, et les confondre nous a coûté 32 défauts dans un connecteur et un double remboursement latent dans un autre.

Si vous maintenez des connecteurs pour des clients et souhaitez une seconde lecture de ce que votre suite ne teste pas, voici comment nous travaillons avec les cabinets de conseil et équipes techniques et avec d'autres partners d'Odoo.

Liens utiles chez FlexigoTech

Travailler avec des partners d'OdooComment nous nous intégrons aux équipes qui ont déjà leur propre cycle de développementIngénierie Odoo pour cabinets de conseilCapacité technique pour les cabinets qui ont besoin de connecteurs maintenusOdoo 19 face aux connecteurs de middlewareOù vit la surface de contrat quand on ajoute une couche intermédiaireConnecteur Mirakl pour OdooComment nous abordons la synchronisation des commandes, retours et règlements

Questions fréquentes

Pourquoi mes tests Odoo passent-ils alors que le connecteur échoue en production ?

Parce que la suite teste votre mock, pas l'API. Le mock, vous l'avez écrit d'après la documentation du jour et il n'a pas bougé depuis ; l'API, si. Un champ lu sous le mauvais nom renvoie None sans lever d'exception, donc aucune assertion ne s'en aperçoit. La seule façon de le détecter est de comparer votre client à la spécification officielle du fournisseur, ou de faire au moins un appel réel au sandbox.

Qu'est-ce que le contract testing appliqué à un connecteur Odoo ?

Dans sa version pratique, il ne nécessite ni broker ni infrastructure : il consiste à garder l'OpenAPI du fournisseur dans le dépôt et à comparer statiquement avec lui les paramètres que vous envoyez, les clés des corps d'écriture et les noms de champ que lit chaque parseur. Trois scripts qui sortent avec un code différent de zéro. Ils couvrent la plupart des défauts de contrat avant même de démarrer Odoo.

Comment savoir si mon mock est désynchronisé de l'API ?

Trois signes : la fixture n'indique pas contre quelle version de la spécification elle a été écrite ; la spécification du fournisseur n'est pas versionnée dans le dépôt, donc vous ne pouvez pas voir son diff ; et aucun test ne touche jamais le réseau. Si les trois sont réunis, votre mock est déjà désynchronisé même si la suite est verte.

Suffit-il de tester le module sur Odoo 19 si je le publie aussi sur 18 et 17 ?

Non. Il existe des constructions qui existent en 19 et qui en 18 ou 17 n'échouent pas : elles sont ignorées. models.Constraint en 18 ou 17 n'est pas une contrainte et ne produit pas d'erreur, donc vous perdez l'unicité en silence. Un ir.cron sans numbercall en 17 s'éteint tout seul après la première exécution. En passant 47 apps sur des installations propres de 17 et 18 Community, 61 des 94 exécutions sont sorties propres : un tiers non.

Un test d'installation est-il la même chose qu'un test d'import ?

Non, et les confondre est l'une des erreurs les plus coûteuses. Importer le module prouve seulement que le Python est valide. L'installer charge le XML, valide les vues, applique la sécurité et exécute les migrations. Et même installé, il faut l'exercer : dans notre propre catalogue, des apps s'installaient parfaitement et plantaient à l'ouverture d'une vue ou à la lecture d'un enregistrement par l'ORM.

Ce que nous faisons à ce sujet

Intégrations sur mesureCe que cela fait, captures, versions et prix.

Vous avez un connecteur dont la suite est verte et vous doutez qu'il fonctionne ?

Nous examinons la surface de contrat contre la spécification réelle du fournisseur et l'installation dans les séries d'Odoo que vous prenez en charge. Écrivez à comercial@flexigobe.com ou appelez le +34 616 809 504.

Parler à un ingénieur