DEV Community

Cover image for Quick Win Card #04 — Le test contrat de 15 lignes qui déverrouille les refacto de schéma sans peur
Michel Faure
Michel Faure

Posted on

Quick Win Card #04 — Le test contrat de 15 lignes qui déverrouille les refacto de schéma sans peur

Quick Win Card #04 strip — Michel hésite à renommer un statut métier, ajoute 15 lignes de test contrat, lance le rename sans peur, CI signale exactement le drift à corriger.

L'accroche

Premier triptyque : trois cartes sur ce qui ment — fichier, compteur, contexte du délégué. Trois hooks défensifs qui forcent l'observable à reparler. Deuxième triptyque, qui s'ouvre ici : non plus ce qui ment, mais ce que ça déverrouille. Quand la défiance est ritualisée, l'audace devient possible. Carte #04 : un test contrat de quinze lignes qui transforme la refacto de schéma — opération qu'on évitait par peur de manquer une référence — en geste banal qu'on fait avant le café.

Ce qui était bloqué

Pendant six semaines, je traînais un renommage évident. Le statut métier eleve aurait dû s'appeler inscrit depuis le début — eleve couvre les anciens, les liste rouge, les sans-réponse, c'est trop large. Mais le statut était utilisé dans la CHECK constraint Postgres, dans une constante TypeScript, dans douze composants UI, dans trois exports Brevo, dans quatre vues SQL, dans deux scripts batch. Un grep en aveugle = oubli garanti. Une PR de renommage = peur du silent break en prod.

La peur n'était pas irrationnelle. Le 11 mai, un import HubSpot avait introduit un statut suspect que la CHECK constraint refusait. Le code TS ne le savait pas — il consommait silencieusement les exceptions Postgres comme des erreurs réseau. Symptôme : trois imports échouent en prod, aucune alerte parce que le try / catch avalait tout. Diagnostic deux jours plus tard. Si je renommais eleve sans synchroniser DB et TS, j'allais reproduire exactement la même classe de drift.

Le test contrat qui déverrouille

À poser dans tests/contracts/statuts.contract.test.ts. Quinze lignes Jest qui font une seule chose : grep la CHECK constraint Postgres, comparer aux valeurs déclarées en constante TypeScript, échouer le build avec un message qui te dit exactement où la dérive est apparue.

import { CONTACT_STATUT_VALID } from '@/lib/contacts'
import { createSupabaseAdmin } from '@/lib/supabase-admin'

test('contacts.statut: DB CHECK matches TS whitelist', async () => {
  const { data } = await createSupabaseAdmin().rpc('introspect_check_values', {
    p_table: 'contacts', p_column: 'statut',
  })
  const db = new Set(data as string[])
  const ts = new Set(CONTACT_STATUT_VALID)
  const dbOnly = [...db].filter(v => !ts.has(v))
  const tsOnly = [...ts].filter(v => !db.has(v))
  if (dbOnly.length || tsOnly.length) {
    throw new Error(`Drift: DB-only=[${dbOnly}], TS-only=[${tsOnly}]`)
  }
})
Enter fullscreen mode Exit fullscreen mode

Le diff de sets est ce qui rend le message d'erreur utile à l'échelle. Avec cinq valeurs ça paraît over-engineered, avec trente l'énumération brute devient illisible et masque exactement ce que le test devrait pointer. Le message Drift: DB-only=[suspect], TS-only=[en_attente] te dit en deux mots où corriger. La fonction introspect_check_values est une RPC Postgres de trois lignes qui parse pg_get_constraintdef en regex ; tu peux aussi la remplacer par un SELECT direct sur pg_constraint si tu préfères tout en SQL.

Ce que ça déverrouille concrètement

Le pattern n'économise pas du temps sur le test lui-même — il coûte quinze lignes à écrire. Le gain est ailleurs, en aval, sur les refacto que tu n'avais plus envie de tenter. Avec ce test dans la CI, le renommage eleve → inscrit s'est fait en trente minutes : changement de la migration DB, changement de la constante TS, push. CI rouge si l'un des deux côtés a oublié l'autre, avec un message qui te dit exactement quelle valeur manque où. Tu corriges, tu repushes, CI vert. La refacto cross-fichier n'est plus un acte de bravoure, c'est un workflow.

Au-delà du renommage, le test ouvre toute une catégorie d'opérations que la peur fermait : ajouter un statut sans risquer d'oublier la TS, retirer un statut sans risquer un crash silencieux à l'INSERT, fusionner deux statuts en un seul avec contrôle automatique du périmètre. Tout ce que tu hésitais à faire parce que « je vais peut-être casser quelque chose ailleurs » devient borné par un message d'erreur explicite. La doctrine défensive du triptyque 1 produit ici son rendement positif : la confiance n'est pas dans l'outil, elle est dans le filet matériel sous l'outil. Tu peux sauter parce que tu as vu le filet de tes propres yeux.

À appliquer maintenant

Identifie dans ton projet une constante TypeScript qui doit matcher une CHECK constraint Postgres (ou un enum DB, ou une whitelist applicative quelconque). Copie le template ci-dessus, adapte le nom de table et de colonne, écris la RPC d'introspection de trois lignes en SQL. Pousse le test dans la CI. La prochaine fois que tu hésites à renommer ou modifier cette colonne, regarde le test. Il fait le grep pour toi. Tu n'as plus à le faire dans ta tête. La refacto que tu repoussais depuis six semaines devient une PR du vendredi après-midi.

Et quand tu délègues la refacto à un sub-agent (cf. QW-03), le test en CI attrape ce que ton brief aurait pu manquer. Le filet tient autant pour ton délégué que pour toi.

Ton quick win tient en cinq minutes — celles qu'il faut pour copier le test, adapter le nom de constante, ajouter la RPC d'introspection. La doctrine du triptyque 1 t'apprenait à te défier des résumés ; celle du triptyque 2 t'apprend à utiliser cette défiance pour t'autoriser ce que tu te refusais.


Quick Win Card series, épisode 04. Ouverture du 2e triptyque : ce que la doctrine déverrouille. Référence ADR-0044 (tests contrat DB↔code) — repo doctrine : github.com/michelfaure/doctrine-counterpart. Suite triptyque pressentie : QW-05 sur le tag [spike] qui déverrouille le prototypage sans dette, QW-06 sur le brief inline qui déverrouille la délégation.

Top comments (0)