Tests de régression IA
Modèles de test spécifiquement conçus pour le développement assisté par IA, où le même modèle écrit et révise le code – créant des angles morts systématiques que seuls les tests automatisés peuvent détecter.
Quand activer
- L'agent IA (Claude Code, Cursor, Codex) a modifié des routes API ou la logique backend
- Un bug a été trouvé et corrigé – besoin d'éviter sa réintroduction
- Le projet dispose d'un mode sandbox/mock exploitable pour des tests sans base de données
- Exécution de
/bug-checkou de commandes de révision similaires après des modifications de code - Plusieurs chemins de code existent (sandbox vs production, flags de fonctionnalité, etc.)
Le problème principal
Lorsque l'IA écrit du code puis révise son propre travail, elle transporte les mêmes hypothèses dans les deux étapes. Cela crée un schéma de défaillance prévisible :
L'IA écrit un correctif → L'IA révise le correctif → L'IA dit "c'est correct" → Le bug existe toujours
Exemple réel (observé en production) :
Correctif 1 : Ajout de notification_settings à la réponse API
→ Oubli de l'ajouter à la requête SELECT
→ L'IA a révisé et ne l'a pas vu (même angle mort)
Correctif 2 : Ajout à la requête SELECT
→ Erreur de build TypeScript (colonne absente des types générés)
→ L'IA a révisé le correctif 1 mais n'a pas vu le problème SELECT
Correctif 3 : Changement pour SELECT *
→ Chemin production corrigé, chemin sandbox oublié
→ L'IA a révisé et ne l'a PAS VU (4ème occurrence)
Correctif 4 : Le test l'a détecté instantanément dès la première exécution PASS :
Le schéma : l'incohérence de chemin sandbox/production est la régression n°1 introduite par l'IA.
Tests d'API en mode sandbox
La plupart des projets avec une architecture compatible IA disposent d'un mode sandbox/mock. C'est la clé pour des tests d'API rapides et sans base de données.
Configuration (Vitest + Next.js App Router)
// vitest.config.ts
import { defineConfig } from "vitest/config";
import path from "path";
export default defineConfig({
test: {
environment: "node",
globals: true,
include: ["__tests__/**/*.test.ts"],
setupFiles: ["__tests__/setup.ts"],
},
resolve: {
alias: {
"@": path.resolve(__dirname, "."),
},
},
});
// __tests__/setup.ts
// Forcer le mode sandbox – pas besoin de base de données
process.env.SANDBOX_MODE = "true";
process.env.NEXT_PUBLIC_SUPABASE_URL = "";
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY = "";
Utilitaire de test pour les routes API Next.js
// __tests__/helpers.ts
import { NextRequest } from "next/server";
export function createTestRequest(
url: string,
options?: {
method?: string;
body?: Record<string, unknown>;
headers?: Record<string, string>;
sandboxUserId?: string;
},
): NextRequest {
const { method = "GET", body, headers = {}, sandboxUserId } = options || {};
const fullUrl = url.startsWith("http") ? url : `http://localhost:3000${url}`;
const reqHeaders: Record<string, string> = { ...headers };
if (sandboxUserId) {
reqHeaders["x-sandbox-user-id"] = sandboxUserId;
}
const init: { method: string; headers: Record<string, string>; body?: string } = {
method,
headers: reqHeaders,
};
if (body) {
init.body = JSON.stringify(body);
reqHeaders["content-type"] = "application/json";
}
return new NextRequest(fullUrl, init);
}
export async function parseResponse(response: Response) {
const json = await response.json();
return { status: response.status, json };
}
Rédaction de tests de régression
Le principe clé : écrire des tests pour les bugs qui ont été trouvés, pas pour le code qui fonctionne.
// __tests__/api/user/profile.test.ts
import { describe, it, expect } from "vitest";
import { createTestRequest, parseResponse } from "../../helpers";
import { GET, PATCH } from "@/app/api/user/profile/route";
// Définir le contrat – quels champs DOIVENT être dans la réponse
const REQUIRED_FIELDS = [
"id",
"email",
"full_name",
"phone",
"role",
"created_at",
"avatar_url",
"notification_settings", // ← Ajouté après avoir trouvé le bug qu'il manquait
];
describe("GET /api/user/profile", () => {
it("retourne tous les champs requis", async () => {
const req = createTestRequest("/api/user/profile");
const res = await GET(req);
const { status, json } = await parseResponse(res);
expect(status).toBe(200);
for (const field of REQUIRED_FIELDS) {
expect(json.data).toHaveProperty(field);
}
});
// Test de régression – ce bug exact a été introduit par l'IA 4 fois
it("notification_settings n'est pas undefined (régression BUG-R1)", async () => {
const req = createTestRequest("/api/user/profile");
const res = await GET(req);
const { json } = await parseResponse(res);
expect("notification_settings" in json.data).toBe(true);
const ns = json.data.notification_settings;
expect(ns === null || typeof ns === "object").toBe(true);
});
});
Tester la parité Sandbox/Production
La régression IA la plus courante : corriger le chemin production mais oublier le chemin sandbox (ou vice versa).
// Tester que les réponses sandbox correspondent au contrat attendu
describe("GET /api/user/messages (liste de conversations)", () => {
it("inclut partner_name en mode sandbox", async () => {
const req = createTestRequest("/api/user/messages", {
sandboxUserId: "user-001",
});
const res = await GET(req);
const { json } = await parseResponse(res);
// Cela a détecté un bug où partner_name avait été ajouté
// au chemin production mais pas au chemin sandbox
if (json.data.length > 0) {
for (const conv of json.data) {
expect("partner_name" in conv).toBe(true);
}
}
});
});
Intégration des tests dans le workflow de vérification des bugs
Définition de commande personnalisée
<!-- .claude/commands/bug-check.md -->
# Vérification de bugs
## Étape 1 : Tests automatisés (obligatoire, ne peut pas être ignoré)
Exécutez ces commandes EN PREMIER avant toute révision de code :
npm run test # Suite de tests Vitest
npm run build # Vérification de type TypeScript + build
- Si les tests échouent → signaler comme bug prioritaire
- Si le build échoue → signaler les erreurs de type comme prioritaires
- Passer à l'étape 2 uniquement si les deux réussissent
## Étape 2 : Révision de code (révision IA)
1. Cohérence des chemins sandbox / production
2. Forme de la réponse API correspond aux attentes du frontend
3. Complétude de la clause SELECT
4. Gestion des erreurs avec rollback
5. Conditions de concurrence dans les mises à jour optimistes
## Étape 3 : Pour chaque bug corrigé, proposer un test de régression
Le workflow
Utilisateur : "バグチェックして" (ou "/bug-check")
│
├─ Étape 1 : npm run test
│ ├─ ÉCHEC → Bug détecté mécaniquement (pas besoin du jugement IA)
│ └─ SUCCÈS → Continuer
│
├─ Étape 2 : npm run build
│ ├─ ÉCHEC → Erreur de type détectée mécaniquement
│ └─ SUCCÈS → Continuer
│
├─ Étape 3 : Révision de code IA (en gardant à l'esprit les angles morts connus)
│ └─ Résultats signalés
│
└─ Étape 4 : Pour chaque correctif, écrire un test de régression
└─ La prochaine vérification détecte si le correctif se casse
Motifs de régression IA courants
Motif 1 : Décalage de chemin Sandbox/Production
Fréquence : Le plus courant (observé dans 3 régressions sur 4)
// ÉCHEC : L'IA ajoute le champ au chemin production uniquement
if (isSandboxMode()) {
return { data: { id, email, name } }; // Nouveau champ manquant
}
// Chemin production
return { data: { id, email, name, notification_settings } };
// SUCCÈS : Les deux chemins doivent retourner la même forme
if (isSandboxMode()) {
return { data: { id, email, name, notification_settings: null } };
}
return { data: { id, email, name, notification_settings } };
Test pour le détecter :
it("sandbox et production retournent les mêmes champs", async () => {
// Dans l'environnement de test, le mode sandbox est forcé à ON
const res = await GET(createTestRequest("/api/user/profile"));
const { json } = await parseResponse(res);
for (const field of REQUIRED_FIELDS) {
expect(json.data).toHaveProperty(field);
}
});
Motif 2 : Omission de clause SELECT
Fréquence : Courant avec Supabase/Prisma lors de l'ajout de nouvelles colonnes
// ÉCHEC : Nouvelle colonne ajoutée à la réponse mais pas à SELECT
const { data } = await supabase
.from("users")
.select("id, email, name") // notification_settings n'est pas ici
.single();
return { data: { ...data, notification_settings: data.notification_settings } };
// → notification_settings est toujours undefined
// SUCCÈS : Utiliser SELECT * ou inclure explicitement les nouvelles colonnes
const { data } = await supabase
.from("users")
.select("*")
.single();
Motif 3 : Fuite d'état d'erreur
Fréquence : Modéré – lors de l'ajout de la gestion des erreurs à des composants existants
// ÉCHEC : État d'erreur défini mais anciennes données non effacées
catch (err) {
setError("Échec du chargement");
// reservations affiche encore les données de l'onglet précédent !
}
// SUCCÈS : Effacer l'état associé en cas d'erreur
catch (err) {
setReservations([]); // Effacer les données obsolètes
setError("Échec du chargement");
}
Motif 4 : Mise à jour optimiste sans rollback approprié
// ÉCHEC : Pas de rollback en cas d'échec
const handleRemove = async (id: string) => {
setItems(prev => prev.filter(i => i.id !== id));
await fetch(`/api/items/${id}`, { method: "DELETE" });
// Si l'API échoue, l'élément a disparu de l'UI mais reste en DB
};
// SUCCÈS : Capturer l'état précédent et rollback en cas d'échec
const handleRemove = async (id: string) => {
const prevItems = [...items];
setItems(prev => prev.filter(i => i.id !== id));
try {
const res = await fetch(`/api/items/${id}`, { method: "DELETE" });
if (!res.ok) throw new Error("Erreur API");
} catch {
setItems(prevItems); // Rollback
alert("Échec de la suppression");
}
};
Stratégie : Tester là où les bugs ont été trouvés
Ne visez pas une couverture à 100 %. Plutôt :
Bug trouvé dans /api/user/profile → Écrire un test pour l'API profile
Bug trouvé dans /api/user/messages → Écrire un test pour l'API messages
Bug trouvé dans /api/user/favorites → Écrire un test pour l'API favorites
Pas de bug dans /api/user/notifications → Ne pas écrire de test (pour l'instant)
Pourquoi cela fonctionne avec le développement IA :
- L'IA a tendance à commettre la même catégorie d'erreur de manière répétée
- Les bugs se regroupent dans les zones complexes (auth, logique multi-chemin, gestion d'état)
- Une fois testée, cette régression exacte ne peut plus se reproduire
- Le nombre de tests croît organiquement avec les corrections de bugs – aucun effort gaspillé
Référence rapide
| Motif de régression IA | Stratégie de test | Priorité |
|---|---|---|
| Décalage sandbox/production | Affirmer la même forme de réponse en mode sandbox | Haute |
| Omission de clause SELECT | Affirmer tous les champs requis dans la réponse | Haute |
| Fuite d'état d'erreur | Affirmer le nettoyage de l'état en cas d'erreur | Moyenne |
| Rollback manquant | Affirmer l'état restauré en cas d'échec API | Moyenne |
| Masquage de null par conversion de type | Affirmer que le champ n'est pas undefined | Moyenne |
À FAIRE / À NE PAS FAIRE
À FAIRE :
- Écrire des tests immédiatement après avoir trouvé un bug (si possible avant de le corriger)
- Tester la forme de la réponse API, pas l'implémentation
- Exécuter les tests comme première étape de chaque vérification de bug
- Garder les tests rapides (< 1 seconde au total avec le mode sandbox)
- Nommer les tests d'après le bug qu'ils préviennent (ex. "régression BUG-R1")
À NE PAS FAIRE :
- Écrire des tests pour du code qui n'a jamais eu de bug
- Faire confiance à l'auto-révision de l'IA comme substitut des tests automatisés
- Sauter les tests de chemin sandbox parce que "ce ne sont que des données mock"
- Écrire des tests d'intégration quand des tests unitaires suffisent
- Viser un pourcentage de couverture – visez la prévention des régressions