Aller au contenu principal
TestsTDDQualitéFormation

Tests logiciels : fondamentaux, pyramide, TDD et BDD

30 min de lecture Tests & Qualité — Chapitre 1

Pourquoi tester, la pyramide des tests, TDD, BDD, types de tests. Les fondamentaux avant d'écrire ta première assertion.

Ce chapitre est gratuit

Pas besoin de compte pour le lire. Decouvre le contenu et decide si le programme est fait pour toi.

Voir les autres chapitres

Pourquoi tester — au-delà du “il faut tester”

Chaque développeur a entendu “il faut écrire des tests”. Peu savent vraiment pourquoi. La réponse standard — “pour éviter les bugs” — passe à côté de l’essentiel. Les tests ne sont pas un filet de sécurité passif. Ils sont un outil de design qui transforme ta façon d’écrire du code.

Un bug détecté en développement coûte 5x moins qu’en staging et 100x moins qu’en production. Mais le vrai gain n’est pas économique — c’est la vélocité. Une équipe avec une suite de tests solide déploie 10 fois par jour sans stress. Une équipe sans tests déploie une fois par mois avec un plan de rollback et le téléphone du CTO en speed dial.

🔥 Cas réel : Une fintech déploie un fix “mineur” en production un vendredi soir. Pas de tests automatisés. Le fix casse le calcul des intérêts — 48h avant que quelqu’un ne le remarque. Coût : 200K€ de corrections, 3 semaines de travail, et la confiance de 500 clients. Un test unitaire de 5 lignes l’aurait détecté en 3 millisecondes.

Concrètement, les tests te donnent :

  • La confiance de refactorer — sans tests, refactorer c’est marcher dans un champ de mines
  • De la documentation vivante — un test ne ment jamais (il échoue quand il est périmé)
  • Un meilleur design — du code testable est du code bien conçu : découplé, responsabilités claires
  • Un filet de sécurité pour l’équipe — le nouveau dev qui touche du code inconnu est protégé

La pyramide des tests — doser l’effort

La pyramide des tests est le modèle de référence pour répartir l’effort de test. Elle définit trois niveaux, chacun avec un rôle précis.

Tests unitaires (70%) — Ils testent une unité isolée : une fonction, une méthode, une classe. Les dépendances externes sont mockées. Ils sont rapides (millisecondes), stables, et tu en écris des centaines.

# Test unitaire — isolé, rapide, déterministe
def calculate_discount(price: float, percentage: float) -> float:
    if percentage < 0 or percentage > 100:
        raise ValueError("Pourcentage entre 0 et 100")
    return price * (1 - percentage / 100)

def test_calculate_discount():
    assert calculate_discount(100, 20) == 80.0

def test_discount_invalid_percentage():
    with pytest.raises(ValueError):
        calculate_discount(100, -10)

Tests d’intégration (20%) — Ils vérifient l’interaction entre composants réels : ton code + la base de données, ton service + une API externe. Plus lents (secondes), mais ils testent les vrais comportements, pas des mocks.

# Test d'intégration — vraie DB, vraie interaction
def test_create_user_in_database(db_session):
    user = UserService(db_session).create_user(
        email="test@example.com", name="Test User"
    )
    saved = db_session.query(User).filter_by(email="test@example.com").first()
    assert saved is not None
    assert saved.name == "Test User"

Tests end-to-end (10%) — Ils simulent le parcours utilisateur complet : un navigateur clique, remplit des formulaires, vérifie des pages. Lents, fragiles, coûteux à maintenir — réserve-les aux scénarios critiques (login, paiement, inscription).

💡 Tip DevOps : L’anti-pattern classique est le “cornet de glace inversé” — trop de tests E2E, pas assez de tests unitaires. Résultat : une suite qui prend 30 minutes, des tests flaky que tout le monde ignore, et un feedback loop inutilisable. Si ta CI met plus de 10 minutes, c’est un signal d’alarme.

🧠 À retenir : Le “trophée des tests” (Kent C. Dodds) est une alternative populaire en frontend : il met l’accent sur les tests d’intégration comme meilleur rapport confiance/coût. La pyramide reste le standard en backend.

TDD — écrire le test d’abord

Le cycle Red-Green-Refactor

Le Test-Driven Development inverse le workflow classique. Au lieu d’écrire le code puis les tests, tu commences par le test qui échoue (Red), tu écris le code minimal pour le faire passer (Green), puis tu améliores la structure sans casser les tests (Refactor).

Ce cycle force un design incrémental. Chaque itération ajoute un comportement, validé par un test. Voici un exemple concret — un calculateur de prix avec TVA et réductions :

# Itération 1 — RED : le test échoue
def test_price_without_discount():
    calc = PriceCalculator()
    assert calc.calculate(100) == 100

# GREEN : code minimal
class PriceCalculator:
    def calculate(self, price):
        return price

# Itération 2 — RED : ajouter la TVA
def test_price_with_vat():
    calc = PriceCalculator(vat_rate=0.20)
    assert calc.calculate(100) == 120

# GREEN : supporter la TVA
class PriceCalculator:
    def __init__(self, vat_rate=0):
        self.vat_rate = vat_rate
    def calculate(self, price):
        return price * (1 + self.vat_rate)

# Itération 3 — RED : gestion d'erreur
def test_negative_price_raises():
    calc = PriceCalculator()
    with pytest.raises(ValueError):
        calc.calculate(-10)

Chaque étape est petite, vérifiable, et documentée par un test. Le refactor vient après — quand tous les tests passent, tu peux restructurer en toute confiance.

Quand TDD brille (et quand il ne brille pas)

TDD excelle sur la logique métier complexe (algorithmes, calculs, règles business), les APIs (le test définit le contrat avant l’implémentation), et les bug fixes (tu écris un test qui reproduit le bug, puis tu corriges).

TDD est moins adapté pour le prototypage (le code va changer radicalement), l’UI (le design évolue trop vite), et l’infrastructure (Terraform, scripts de déploiement).

⚠️ Attention : TDD n’est pas dogmatique. Personne ne suit les trois lois de Uncle Bob à 100%. Le principe — écrire le test d’abord — est puissant pour le design. L’appliquer rigidement sur chaque getter est contre-productif.

BDD — le pont entre technique et métier

Le Behavior-Driven Development étend TDD en écrivant les tests dans un langage naturel que tout le monde comprend — développeurs, product owners, testeurs. Le format Gherkin (Given/When/Then) décrit des scénarios métier :

Feature: Processus de paiement

  Scenario: Paiement réussi avec carte valide
    Given j'ai un produit "Laptop" à 999€ dans mon panier
    And j'ai une carte de crédit valide
    When je procède au paiement
    Then la commande est créée avec le statut "confirmée"
    And je reçois un email de confirmation

  Scenario Outline: Réduction selon le montant
    Given j'ai des produits pour <montant>€
    When je procède au paiement
    Then la réduction est de <reduction>%

    Examples:
      | montant | reduction |
      | 50      | 0         |
      | 100     | 5         |
      | 500     | 15        |

Ces scénarios sont exécutables — des step definitions Python (ou JS, Java) les connectent au code réel. L’avantage : le PO peut lire et valider les scénarios, et ils servent de documentation vivante du comportement attendu.

🔥 Cas réel : Une équipe de 15 devs sur un projet e-commerce utilise BDD pour les parcours critiques (inscription, panier, paiement). Les scénarios Gherkin sont dans le repo Git, écrits conjointement avec le PO. Résultat : les specs sont toujours à jour parce qu’elles sont les tests. Zéro divergence entre ce qui est documenté et ce qui est implémenté.

BDD ne remplace pas TDD — il le complète. Les scénarios BDD sont des tests d’acceptance de haut niveau. Les tests TDD unitaires supportent l’implémentation en dessous.

Écrire de bons tests — patterns et pièges

Le pattern AAA

Chaque test suit la même structure : Arrange (prépare le contexte), Act (exécute l’action), Assert (vérifie le résultat) :

def test_user_can_change_email():
    # Arrange
    user = User(email="old@example.com")
    # Act
    user.change_email("new@example.com")
    # Assert
    assert user.email == "new@example.com"

Les pièges classiques

Tester l’implémentation plutôt que le comportement — Si tu vérifies qu’une méthode interne est appelée plutôt que le résultat produit, ton test casse à chaque refactor. Teste ce que le code fait, pas comment il le fait.

Tests flaky — Un test qui dépend de l’heure, d’un service externe, ou d’un ordre d’exécution échouera aléatoirement. Injecte les dépendances : get_greeting(hour=9) plutôt que get_greeting() qui appelle datetime.now().

Ignorer les cas d’erreur — Tester le happy path uniquement, c’est tester 50% du code. Les bugs vivent dans les edge cases : division par zéro, input vide, timeout réseau.

100% de couverture — La couverture de code mesure quelles lignes sont exécutées, pas si les assertions sont pertinentes. 80% de couverture avec des tests de qualité vaut mieux que 100% avec des assertions vides.

# Mesurer la couverture
pytest --cov=src --cov-report=term-missing

💡 Tip DevOps : Le mutation testing va plus loin que la couverture. Il modifie ton code (change >= en >, + en -) et vérifie que tes tests échouent. Si une mutation passe inaperçue, tu as un trou dans ta couverture. Outils : mutmut (Python), Stryker (JS), PIT (Java).

Résumé

Les tests ne sont pas une corvée — c’est un outil de design qui améliore ton code, ta confiance et ta vélocité de déploiement. Le coût d’un bug augmente exponentiellement avec le temps : détecte-le au plus tôt.

La pyramide des tests guide la répartition : 70% unitaires (rapides, stables), 20% intégration (vrais composants), 10% E2E (scénarios critiques). L’anti-pattern du cornet de glace inversé — trop de E2E, pas assez de unitaires — tue la productivité.

TDD (Red-Green-Refactor) force un design incrémental guidé par les tests. Il excelle sur la logique métier et les APIs. BDD étend TDD avec des scénarios en langage naturel que le métier peut lire et valider.

Un bon test est rapide, isolé, déterministe et teste un comportement (pas une implémentation). Il suit le pattern AAA et couvre les cas d’erreur autant que le happy path.

🧠 À retenir : Le vrai ROI des tests ne se mesure pas en bugs évités — il se mesure en déploiements faits sereinement, en refactors sans peur, et en nuits sans page d’urgence.

Articles liés