TDD assisté par IA : workflow réaliste pour développeurs

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 True

Ce 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 == 1

Vous validez

pytest test_iban_validator.py -v

Si 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émentation

4. 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 == 1

L'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_percent

Outils 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é