Table-driven tests en Go : le pattern que tout le monde utilise et que personne ne maîtrise
Ou : Comment un slice de structs est devenu le test de Turing des développeurs Go
Il y a un pattern en Go que tout le monde connaît, que tout le monde utilise, et que personne ne remet jamais en question. Le table-driven test. Tu ouvres un fichier _test.go dans n’importe quel projet Go sérieux, tu tombes dessus. Tu demandes à Claude, Copilot ou Cursor d’écrire des tests, ils te le sortent en trois secondes. C’est le SELECT * FROM du testing Go : la première chose que tout le monde apprend, et la dernière chose que tout le monde maîtrise vraiment.
Dans l’article précédent, on a posé le manifeste : en 2026, ton boulot n’est plus d’écrire du code, c’est de valider celui que l’IA produit. Aujourd’hui, on passe à la pratique. Et on commence par le pattern le plus basique, le plus omniprésent, et — paradoxalement — le plus mal utilisé du testing en Go.
Table-driven tests Go et IA : anatomie du pattern que les LLM imitent sans comprendre
Ce qu’est un table-driven test (et pourquoi Go adore ça)
L’idée est d’une simplicité désarmante. Au lieu d’écrire une fonction de test par cas, tu mets tous tes cas dans un tableau — un slice de structs, pour être précis — et tu itères dessus1. Comme ça :
| |
C’est propre. C’est lisible. Tu ajoutes un cas ? Une ligne dans le tableau. Tu veux savoir ce qui est testé ? Tu lis le tableau. Tu veux lancer un seul cas ? go test -run TestAbs/négatif. Le wiki officiel de Go recommande ce pattern depuis des années2, et il y a de bonnes raisons : ça force à séparer les données du test de la logique du test. C’est de la programmation déclarative appliquée au testing.
Le problème, c’est que l’IA a très bien compris la forme du pattern. Ce qu’elle n’a pas compris, c’est le fond.
Ce que l’IA te génère (et pourquoi c’est un piège)
Prenons un cas concret. Tu as une fonction qui parse un montant financier à partir d’une chaîne de caractères :
| |
Tu demandes à ton LLM préféré : « Écris des table-driven tests pour ParseAmount. » Tu obtiens quelque chose dans ce goût-là :
| |
Ça compile. Ça passe. Le format est irréprochable. Ton instinct te dit « c’est bien ». Ton CI aussi.
Sauf que ce test est un château de cartes.
Les cinq péchés capitaux du table-driven test généré par IA
1. Les edge cases fantômes
Le test ci-dessus ne vérifie pas :
"12.999"— trois décimales.int64(12.999 * 100)donne1299, pas1300. Ton utilisateur qui paie 12,999 € se fait arrondir vers le bas, silencieusement3."0.1"+"0.2"— le classique IEEE 754.0.1 + 0.2ne vaut pas0.3en float64. Tu convertis en centimes via des floats ? Bienvenue dans un monde de micro-erreurs d’arrondi4." 12.50 "— avec des espaces. LeTrimSpaceest là, mais est-il testé ? Non."12.50€"— avec un symbole de devise. Ça crashe ? Ça retourne zéro ? Personne ne sait."999999999999.99"— les grands montants. Ça tient dans unint64? Oui5. Mais l’IA ne l’a pas vérifié.
L’IA teste ce qu’elle pense être des edge cases. Elle teste « invalide » et « vide » parce que c’est ce que disent les tutoriels. Mais les vrais edge cases — ceux qui cassent en production — nécessitent de comprendre le domaine métier, pas juste la signature de la fonction.
2. Les noms de cas qui ne disent rien
"montant simple", "avec virgule", "invalide" — ce sont des noms de cas. Pas des descriptions de comportement.
Quand un test échoue en CI à 3h du matin, tu vois :
| |
Super. « Montant simple » a échoué. Qu’est-ce que ça veut dire ? Tu ouvres le fichier, tu cherches le cas, tu relis le code. Trois minutes de perdu, multipliées par le nombre de tests qui pètent.
Un bon nom de cas, c’est une spécification :
| |
Quand virgule_convertie_en_point échoue, tu sais exactement quel comportement est cassé, sans ouvrir le fichier. C’est le genre de détail que l’IA considère comme cosmétique et que toi, à 3h du matin, tu considères comme vital6.
3. Les assertions wantErr bool
Le pattern wantErr bool — « je m’attends à une erreur, ou pas » — est le minimum syndical. L’IA l’adore parce qu’il est simple à générer. Mais il ne vérifie pas quelle erreur.
Ta fonction retourne fmt.Errorf("montant invalide: %q", s). Est-ce que le message contient l’input ? Est-ce que c’est un type d’erreur spécifique qu’un appelant peut inspecter avec errors.Is ? On n’en sait rien — le test dit juste « il y a une erreur » ou « il n’y en a pas ».
Pour du code critique, la struct de test devrait inclure un wantErrContains string ou un wantErrType error :
| |
4. La structure figée
L’IA génère un seul tableau pour tous les cas. Mais parfois, tu testes deux comportements très différents : le parsing normal et la gestion d’erreurs. Deux tableaux, deux boucles, c’est plus clair :
| |
C’est plus long ? Oui. C’est aussi plus lisible, plus maintenable, et chaque cas a exactement les champs qu’il utilise — pas de wantErr bool inutile dans les cas valides, pas de want int64 inutile dans les cas d’erreur7.
5. L’absence du cas « production réelle »
L’IA teste avec des données inventées. "10.00", "abc", "42" — c’est synthétique. Tes vrais utilisateurs vont envoyer "12,50 €", "1 250,00" (séparateur de milliers), "$12.50", ou "-0.00".
Un bon tableau de tests contient au moins 2-3 cas tirés de données réelles — des trucs que tu as vus dans tes logs, dans tes tickets de bug, dans les imports CSV de tes clients. L’IA ne peut pas inventer ces cas, parce qu’elle ne connaît pas ton contexte métier. C’est ton boulot.
Slice de structs vs map : le faux débat
Tu verras parfois des table-driven tests écrits avec une map[string]struct{...} au lieu d’un slice :
| |
L’IA choisit l’un ou l’autre selon son humeur. La vraie réponse : utilise un slice. Toujours8.
Pourquoi ? Parce qu’une map en Go n’a pas d’ordre d’itération garanti. Tes tests s’exécutent dans un ordre aléatoire à chaque run. C’est acceptable en théorie (un test ne devrait pas dépendre de l’ordre), mais en pratique, quand tu debugges un échec intermittent à 3h du matin — oui, encore — tu veux que les tests s’exécutent dans l’ordre où tu les lis. Le slice te donne ça. La map, non.
La seule exception légitime : quand tes cas de test n’ont pas besoin de noms (rare) et que tu testes une fonction pure sans état. Même là, le slice reste plus lisible.
La méthode des 60 secondes : reviewer un table-driven test IA
Voici comment évaluer un table-driven test généré par l’IA en moins d’une minute. Cinq questions, dans l’ordre :
1. Les noms de cas décrivent-ils un comportement ? (10 secondes)
Lis la colonne name. Si tu vois "test1", "basic", "valid", "invalid" — c’est de la décoration. Chaque nom doit répondre à la question « quel comportement est vérifié ici ? ».
2. Les edge cases du domaine sont-ils présents ? (15 secondes) Regarde les inputs. Est-ce qu’il y a des valeurs limites ? Des chaînes vides, des nil, des zéros, des nombres négatifs, des très grands nombres ? Si tous les cas sont du « happy path avec des variantes cosmétiques », le tableau est creux.
3. La struct a-t-elle les bons champs ? (10 secondes)
Est-ce que chaque champ est utilisé par chaque cas ? Si la moitié des cas ont wantErr: false et n’utilisent jamais le champ erreur, il faut peut-être deux tableaux. Des champs inutilisés, c’est du bruit.
4. Les assertions vérifient-elles le bon niveau ? (15 secondes)
wantErr bool ne suffit pas pour du code critique. got != tt.want ne suffit pas si le résultat est un objet complexe. Cherche les comparaisons superficielles — elles cachent des bugs.
5. Manque-t-il un cas de production réelle ? (10 secondes)
Si tous les inputs sont "abc", "test@test.com", "42" — c’est du testing de tutoriel. Ajoute au moins un cas tiré d’un vrai scénario utilisateur.
Si tu réponds « non » à deux questions ou plus, le test a besoin de travail. Et c’est exactement pour ça qu’on te paie9.
Ce que ça change dans ta pratique quotidienne
Le table-driven test n’est pas juste un pattern d’organisation. C’est un outil de pensée. Quand tu structures tes cas dans un tableau, tu es forcé de répondre à des questions que le code source ne pose pas :
- Quels sont les vrais inputs possibles ?
- Qu’est-ce qui se passe aux frontières ?
- Quel comportement est garanti, et lequel est un accident d’implémentation ?
L’IA, elle, remplit le tableau comme on remplit un formulaire : mécaniquement, sans se poser ces questions. C’est pour ça que ses table-driven tests ont toujours l’air corrects et sont rarement suffisants.
Ta valeur ajoutée, c’est le contenu du tableau, pas sa forme. La forme, l’IA la maîtrise. Le contenu — les cas qui comptent, les noms qui documentent, les edge cases qui sauvent — c’est toi.
Ce que tu peux faire maintenant
- Ouvre un fichier
_test.godans ton projet Go actuel. - Trouve un table-driven test — il y en a forcément un.
- Applique la méthode des 60 secondes.
- Ajoute les cas manquants. Renomme les cas vagues. Sépare les tableaux si nécessaire.
- Lance
go test -v -run NomDuTestet vérifie que les noms de cas dans la sortie racontent une histoire lisible.
Si tes noms de cas forment une spécification quand tu les lis de haut en bas, tu as un bon tableau. Si ça ressemble à une liste de courses écrite par quelqu’un d’autre, tu as du travail.
La suite
Le prochain article de la série plonge dans t.Run() et les subtests : comment isoler, paralléliser, et filtrer tes tests pour debugger efficacement quand quelque chose casse. Parce que le tableau, c’est bien. Mais savoir naviguer dans le tableau quand un cas échoue à 3h du matin — oui, c’est un thème récurrent — c’est mieux.
Le pattern est documenté dans le Go Wiki — Table Driven Tests. C’est devenu le pattern par défaut en Go, au point que les outils d’IA le génèrent automatiquement pour n’importe quelle fonction. ↩︎
Le wiki officiel Go maintient une page dédiée aux table-driven tests depuis les premières versions du langage. Le pattern est aussi recommandé dans le livre The Go Programming Language de Donovan et Kernighan (2015). ↩︎
Les erreurs d’arrondi en virgule flottante sont l’un des bugs les plus courants en finance.
12.999 * 100donne1299.9000000000001en IEEE 754, etint64()tronque vers zéro. La solution classique : parser la partie entière et décimale séparément, ou utiliser une bibliothèque commeshopspring/decimal. ↩︎Le problème
0.1 + 0.2 ≠ 0.3est une conséquence de la représentation binaire des nombres à virgule flottante (IEEE 754). En Go, avec des variablesfloat64:a, b := 0.1, 0.2; fmt.Println(a + b == 0.3)affichefalse(le compilateur Go évalue les constantes littérales avec une précision arbitraire, mais à l’exécution les float64 reprennent leurs droits). C’est le genre de bug que l’IA ne teste jamais parce que, techniquement, elle « sait » que les floats sont imprécis — mais elle ne fait pas le lien avec ton cas d’usage. ↩︎Un
int64va jusqu’à 9 223 372 036 854 775 807. En centimes, ça fait ~92 233 720 368 547 758 €. Tu as de la marge. Sauf si tu travailles pour un gouvernement qui imprime de la monnaie. ↩︎Le guide officiel Go sur les tests recommande des noms de cas descriptifs pour faciliter le debugging :
go test -vaffiche chaque sous-test avec son nom. Un bon nom de cas est une mini-documentation du comportement attendu. ↩︎Séparer les cas valides et les cas d’erreur en deux slices est un pattern défendu par plusieurs articles de la communauté Go. L’avantage principal : chaque struct n’a que les champs nécessaires, ce qui élimine les champs « toujours à zéro » qui polluent la lecture. ↩︎
La recommandation d’utiliser des slices plutôt que des maps pour les table-driven tests est un consensus de la communauté Go. L’ordre d’itération non garanti des maps est documenté dans la spécification du langage et expliqué en détail sur le blog officiel Go. ↩︎
Cette méthode de review rapide est une synthèse de plusieurs pratiques recommandées dans la communauté Go testing. L’idée de base : si tu ne peux pas évaluer la qualité d’un test en le survolant, le test est mal structuré — et c’est exactement ce qui arrive avec 90% des tests générés par IA. ↩︎