TDD assisté par IA : workflow réaliste pour développeurs
Le TDD (Test-Driven Development) et l'IA semblent contradictoires. Le TDD demande de réfléchir avant de coder, l'IA génère du code instantanément. Pourtant, bien combinés, ils forment un workflow plus rapide et plus fiable que chacun séparément. Voici comment les intégrer intelligemment.
Le workflow TDD assisté par IA
Vue d'ensemble
┌─────────────────────────────────────────────────────────────┐
│ CYCLE TDD + IA │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. RED : Écrire le test (VOUS) │
│ ↓ │
│ 2. GREEN : Implémenter (IA génère, VOUS validez) │
│ ↓ │
│ 3. REFACTOR : Améliorer (IA propose, VOUS décidez) │
│ ↓ │
│ 4. Répéter │
│ │
└─────────────────────────────────────────────────────────────┘Principe fondamental
L'humain garde le contrôle sur le QUOI, l'IA aide sur le COMMENT.
| Étape | Responsable | Pourquoi |
|---|---|---|
| Définir le comportement attendu | Humain | Compréhension métier |
| Écrire le test | Humain | Spécification précise |
| Générer l'implémentation | IA | Rapidité, patterns connus |
| Valider l'implémentation | Humain | Qualité, sécurité |
| Refactorer | IA + Humain | Suggestions + décision |
Étape 1 : RED - Écrire le test (vous, pas l'IA)
Pourquoi vous devez écrire le test
Le test EST la spécification. Si l'IA écrit le test, elle définit le comportement attendu. Vous perdez :
- La réflexion sur les edge cases
- La compréhension profonde du besoin
- Le contrôle sur ce que le code doit faire
Exemple concret
Besoin : Une fonction qui valide un IBAN français.
import pytest
from iban_validator import validate_french_iban
class TestFrenchIbanValidation:
"""Tests pour la validation d'IBAN français."""
def test_valid_iban_returns_true(self):
"""Un IBAN français valide doit retourner True."""
valid_iban = "FR76 3000 6000 0112 3456 7890 189"
assert validate_french_iban(valid_iban) is True
def test_valid_iban_without_spaces(self):
"""Un IBAN sans espaces doit être accepté."""
valid_iban = "FR7630006000011234567890189"
assert validate_french_iban(valid_iban) is True
def test_invalid_checksum_returns_false(self):
"""Un IBAN avec checksum invalide doit retourner False."""
invalid_iban = "FR76 3000 6000 0112 3456 7890 999"
assert validate_french_iban(invalid_iban) is False
def test_non_french_iban_returns_false(self):
"""Un IBAN non-français doit retourner False."""
german_iban = "DE89 3704 0044 0532 0130 00"
assert validate_french_iban(german_iban) is False
def test_empty_string_returns_false(self):
"""Une chaîne vide doit retourner False."""
assert validate_french_iban("") is False
def test_none_raises_type_error(self):
"""None doit lever une TypeError."""
with pytest.raises(TypeError):
validate_french_iban(None)
def test_iban_too_short_returns_false(self):
"""Un IBAN trop court doit retourner False."""
assert validate_french_iban("FR76 3000") is False
def test_iban_with_lowercase_is_valid(self):
"""Un IBAN en minuscules doit être accepté."""
valid_iban = "fr7630006000011234567890189"
assert validate_french_iban(valid_iban) is TrueCe que vous avez défini :
- Le nom de la fonction et sa signature
- Les cas valides et invalides
- Le comportement sur les edge cases (None, vide, minuscules)
- Les exceptions attendues
Étape 2 : GREEN - L'IA génère l'implémentation
Le prompt efficace
Voici mes tests pour une fonction validate_french_iban :
[coller les tests]
Génère l'implémentation qui fait passer tous ces tests.
Contraintes :
- Python 3.11+
- Pas de dépendances externes
- Code lisible et documentéCe que l'IA génère
def validate_french_iban(iban: str) -> bool:
"""
Valide un IBAN français.
Args:
iban: L'IBAN à valider (avec ou sans espaces)
Returns:
True si l'IBAN est valide, False sinon
Raises:
TypeError: Si iban n'est pas une chaîne
"""
if iban is None:
raise TypeError("IBAN cannot be None")
if not isinstance(iban, str):
raise TypeError("IBAN must be a string")
# Normaliser : supprimer espaces et mettre en majuscules
iban = iban.replace(" ", "").upper()
# Vérifier que c'est un IBAN français
if not iban.startswith("FR"):
return False
# Vérifier la longueur (27 caractères pour la France)
if len(iban) != 27:
return False
# Vérifier le format : FR + 2 chiffres + 23 alphanumériques
if not iban[2:4].isdigit():
return False
# Algorithme de validation IBAN (modulo 97)
# Déplacer les 4 premiers caractères à la fin
rearranged = iban[4:] + iban[:4]
# Convertir les lettres en chiffres (A=10, B=11, ..., Z=35)
numeric = ""
for char in rearranged:
if char.isdigit():
numeric += char
else:
numeric += str(ord(char) - ord('A') + 10)
# Vérifier que le modulo 97 égale 1
return int(numeric) % 97 == 1Vous validez
pytest test_iban_validator.py -vSi un test échoue : demandez à l'IA de corriger en lui montrant l'erreur.
Étape 3 : REFACTOR - Améliorer ensemble
Demander des suggestions
Le code fonctionne. Propose des améliorations :
- Performance
- Lisibilité
- Robustesse
Ne change pas le comportement (les tests doivent toujours passer).L'IA propose
FRENCH_IBAN_LENGTH = 27
FRENCH_COUNTRY_CODE = "FR"
IBAN_MODULO = 97
def _letter_to_number(char: str) -> str:
"""Convertit une lettre en sa valeur numérique IBAN."""
return str(ord(char) - ord('A') + 10)
def validate_french_iban(iban: str) -> bool:
if iban is None:
raise TypeError("IBAN cannot be None")
iban = _normalize_iban(iban)
if not _is_french_iban(iban):
return False
if not _has_valid_length(iban):
return False
return _has_valid_checksum(iban)Vous décidez
- Suggestion 1 (constantes) : ✅ Acceptée
- Suggestion 2 (fonction extraite) : ✅ Acceptée
- Suggestion 3 (early returns) : ⚠️ Partiellement - gardez la lisibilité sans sur-découper
Relancez les tests après chaque refactoring.
Ce que l'IA ne doit JAMAIS faire
1. Écrire les tests à votre place
prompt = "Écris des tests pour une fonction de validation d'email"Pourquoi c'est dangereux :
- L'IA ne connaît pas vos contraintes métier
- Elle teste ce qu'elle sait implémenter facilement
- Les edge cases critiques sont souvent oubliés
2. Modifier les tests pour faire passer le code
"Le test échoue. Modifie le test pour qu'il passe."3. Générer du code sans tests existants
1. Demander à l'IA de générer une feature complète
2. Écrire les tests après
3. Modifier les tests si ça ne marche pas
1. Écrire les tests d'abord
2. Demander à l'IA d'implémenter
3. Si ça ne passe pas, l'IA corrige l'implémentation4. Faire confiance aveuglément au code généré
def process_payment(amount, card_number):
# Semble correct...
return payment_gateway.charge(card_number, amount)Toujours relire le code généré, surtout pour :
- Sécurité (injection, auth, crypto)
- Gestion d'erreurs
- Edge cases métier
5. Ignorer les tests de mutation
pip install mutmut
mutmut run --paths-to-mutate=src/Workflow complet : exemple pas à pas
Contexte
Implémenter un panier d'achat avec promotions.
Itération 1 : Ajouter un produit
Vous écrivez le test :
class TestShoppingCart:
def test_add_single_product(self):
cart = ShoppingCart()
cart.add("PROD-001", quantity=1, unit_price=29.99)
assert cart.total == 29.99
assert cart.item_count == 1L'IA implémente :
class ShoppingCart:
def __init__(self):
self._items = {}
def add(self, product_id: str, quantity: int, unit_price: float):
if product_id in self._items:
self._items[product_id]["quantity"] += quantity
else:
self._items[product_id] = {
"quantity": quantity,
"unit_price": unit_price
}
@property
def total(self) -> float:
return sum(
item["quantity"] * item["unit_price"]
for item in self._items.values()
)
@property
def item_count(self) -> int:
return sum(item["quantity"] for item in self._items.values())Itération 2 : Appliquer une promotion
Vous ajoutez un test :
def test_apply_percentage_discount(self):
cart = ShoppingCart()
cart.add("PROD-001", quantity=2, unit_price=50.00)
cart.apply_discount("SUMMER20", discount_percent=20)
assert cart.total == 80.00 # 100 - 20%
assert cart.discount_applied == "SUMMER20"L'IA étend l'implémentation :
def apply_discount(self, code: str, discount_percent: float):
if self._discount_code is not None:
raise ValueError("A discount is already applied")
self._discount_code = code
self._discount_percent = discount_percent
@property
def total(self) -> float:
subtotal = sum(
item["quantity"] * item["unit_price"]
for item in self._items.values()
)
if self._discount_percent:
subtotal *= (1 - self._discount_percent / 100)
return round(subtotal, 2)Itération 3 : Edge case - promotion sur panier vide
Vous anticipez un problème :
def test_apply_discount_to_empty_cart_raises(self):
cart = ShoppingCart()
with pytest.raises(ValueError, match="Cannot apply discount to empty cart"):
cart.apply_discount("SUMMER20", discount_percent=20)L'IA corrige :
def apply_discount(self, code: str, discount_percent: float):
if not self._items:
raise ValueError("Cannot apply discount to empty cart")
if self._discount_code is not None:
raise ValueError("A discount is already applied")
self._discount_code = code
self._discount_percent = discount_percentOutils recommandés
IDE avec IA intégrée
| Outil | Points forts | TDD-friendly |
|---|---|---|
| GitHub Copilot | Complétion contextuelle | ⭐⭐⭐ |
| Cursor | Chat intégré, multi-fichiers | ⭐⭐⭐⭐ |
| Claude Code | Compréhension projet | ⭐⭐⭐⭐ |
| Cody (Sourcegraph) | Contexte codebase | ⭐⭐⭐ |
Configuration pour TDD
// settings.json (VS Code + Copilot)
{
// Désactiver les suggestions automatiques dans les fichiers de test
"github.copilot.enable": {
"*": true,
"**/*test*.py": false,
"**/*spec*.ts": false
}
}Prompts réutilisables
TDD_IMPLEMENT = """
Voici mes tests :
{tests}
Génère l'implémentation minimale qui fait passer tous ces tests.
- Ne génère que le code nécessaire
- Pas de fonctionnalités supplémentaires
- Docstrings concises
"""
TDD_REFACTOR = """
Ce code passe tous les tests :
{code}
Propose des améliorations (lisibilité, performance) sans changer le comportement.
Les tests existants doivent toujours passer.
"""
TDD_EDGE_CASES = """
Voici ma fonction et ses tests actuels :
{code_and_tests}
Quels edge cases ne sont pas couverts ?
Liste uniquement, ne génère pas les tests.
"""Métriques de succès
Ce que vous devez mesurer
metrics = {
# Couverture
"line_coverage": ">= 80%",
"branch_coverage": ">= 70%",
"mutation_score": ">= 60%",
# Qualité
"tests_written_by_human": "100%",
"implementation_generated_by_ai": "variable",
"ai_code_reviewed": "100%",
# Vélocité
"time_to_first_test": "before implementation",
"red_green_refactor_cycles": "tracked",
}Anti-métriques
- ❌ "Temps gagné en laissant l'IA écrire les tests"
- ❌ "Couverture atteinte en générant des tests après coup"
- ❌ "Lignes de code produites par heure"
Conclusion
Le TDD assisté par IA fonctionne quand vous respectez une règle simple : vous spécifiez, l'IA implémente.
Les tests sont votre contrat. Ils définissent ce que le code doit faire. L'IA est votre assistant qui traduit ce contrat en code. Si vous inversez les rôles, vous perdez le contrôle et la qualité.
Workflow en une phrase : Écrivez le test, demandez l'implémentation, validez, refactorez. Répétez.
L'IA accélère le TDD, elle ne le remplace pas.
Pour approfondir l'intégration IA dans le développement : TDD assisté par IA : comment tester plus vite sans sacrifier la qualité