10 Tests, sécurité et qualité du code

Tests unitaires, tests d’intégration et couverture

10.1 Génération de tests unitaires 🧪

Définition

Un test unitaire vérifie le comportement d’une petite unité de code isolée :

  • une fonction ;
  • une méthode ;
  • une classe ;
  • un service ;
  • un helper.

L’objectif est de vérifier que cette unité se comporte correctement dans différents cas.

Pourquoi Copilot est utile ?

Copilot peut aider à :

  • générer rapidement des cas de test ;
  • proposer des edge cases ;
  • suivre le style des tests existants ;
  • écrire des assertions ;
  • produire des tests pour des erreurs ;
  • identifier des cas oubliés.

Pour le GH-300

Le GH-300 ne te demande pas la syntaxe de pytest ou de Jest. Il te demande ce que tu demandes à Copilot (quel type de test, avec quel contexte, pour quels cas) et ce que tu vérifies ensuite. Les exemples de code de ce chapitre illustrent ce raisonnement : le langage ou le framework change, la décision reste la même.

Quel outil Copilot pour quelle tâche de test ?

Les outils sont décrits aux chapitres 3 et 4 ; ici, seul le choix change.

Tâche de testOutil
Explorer les cas à couvrir ou choisir un framework, sans rien modifierAsk
Concevoir une stratégie de tests sur plusieurs fichiers avant d’écrire du codePlan
Créer le projet de tests, écrire, exécuter et corriger en boucleAgent
Ajouter des cas à un fichier de tests qui montre déjà le motif à suivreGhost text (suggestions inline)
Corriger un test qui échoueFix Test Failure ou /fixTestFailure (VS Code)

Plus la tâche est floue ou touche plusieurs fichiers, plus on planifie avant d’exécuter. Quel que soit l’outil, on relit le diff et on exécute les tests : des tests générés ne prouvent rien tant qu’ils n’ont pas été relus et lancés.

Exemple simple

Fonction :

py
def calculate_discount(price: float, percentage: float) -> float:
    if price < 0:
        raise ValueError("price must be non-negative")

    if percentage < 0 or percentage > 100:
        raise ValueError("percentage must be between 0 and 100")

    return price - price * (percentage / 100)

Mauvais prompt :

Texte
Write tests.

Bon prompt :

Texte
Generate pytest unit tests for this function.

Cover:
- valid discount
- zero discount
- 100 percent discount
- negative price
- negative percentage
- percentage greater than 100

Do not modify the production code.

Tests possibles :

pyTest proposé
import pytest

def test_calculate_discount_with_valid_percentage():
    assert calculate_discount(100, 20) == 80

def test_calculate_discount_with_zero_percentage():
    assert calculate_discount(100, 0) == 100

def test_calculate_discount_with_full_discount():
    assert calculate_discount(100, 100) == 0

def test_calculate_discount_rejects_negative_price():
    with pytest.raises(ValueError, match="price must be non-negative"):
        calculate_discount(-100, 20)

def test_calculate_discount_rejects_negative_percentage():
    with pytest.raises(ValueError, match="percentage must be between 0 and 100"):
        calculate_discount(100, -1)

def test_calculate_discount_rejects_percentage_over_100():
    with pytest.raises(ValueError, match="percentage must be between 0 and 100"):
        calculate_discount(100, 101)

Exemple TypeScript avec Jest

Fonction :

ts
export function normalizeEmail(email: string): string {
  const normalizedEmail = email.trim().toLowerCase();

  if (!normalizedEmail) {
    throw new Error("Email must not be empty");
  }

  return normalizedEmail;
}

Prompt :

Texte
Generate Jest unit tests for normalizeEmail.

Cover:
- trimming spaces
- lowercasing
- already normalized email
- empty string
- whitespace-only string

Tests possibles :

tsTest proposé
import { normalizeEmail } from "./normalizeEmail";

describe("normalizeEmail", () => {
  it("trims surrounding spaces", () => {
    expect(normalizeEmail(" user@example.com ")).toBe("user@example.com");
  });

  it("lowercases the email", () => {
    expect(normalizeEmail("USER@EXAMPLE.COM")).toBe("user@example.com");
  });

  it("keeps an already normalized email unchanged", () => {
    expect(normalizeEmail("user@example.com")).toBe("user@example.com");
  });

  it("throws an error for an empty string", () => {
    expect(() => normalizeEmail("")).toThrow("Email must not be empty");
  });

  it("throws an error for whitespace-only input", () => {
    expect(() => normalizeEmail("   ")).toThrow("Email must not be empty");
  });
});

Bon prompt pour tests unitaires

Texte
Generate unit tests for the selected function.

Use [test framework].

Cover:
- normal cases
- edge cases
- invalid inputs
- boundary values
- expected exceptions

Constraints:
- do not modify production code
- follow the existing test style
- explain any missing test cases

Piège d’examen

Des tests générés qui passent tous ne prouvent pas que le comportement est correct. Copilot déduit souvent les valeurs attendues du code existant : si le code contient un bug, les tests peuvent le reproduire. Donne à Copilot la règle attendue (la spécification, un exemple de résultat) et relis les valeurs attendues, pas seulement le fait que les tests passent.

À retenir pour l’examen

Si le scénario dit :

Un développeur veut vérifier le comportement d’une fonction isolée.

La bonne réponse est souvent :

Utiliser Copilot pour générer des tests unitaires couvrant cas normaux, edge cases et erreurs attendues.

10.2 Génération de tests d’intégration 🔗

Définition

Un test d’intégration vérifie que plusieurs composants fonctionnent correctement ensemble.

Exemples :

  • endpoint API + service + repository ;
  • service + base de données ;
  • contrôleur + middleware auth ;
  • application + système externe mocké ;
  • pipeline de traitement complet.

Différence test unitaire vs test d’intégration

Type de testObjectif
Test unitaireVérifier une unité isolée
Test d’intégrationVérifier l’interaction entre plusieurs composants

Exemple Express API

Endpoint conceptuel :

ts
app.post("/users", validateUserInput, async (req, res) => {
  const user = await userService.createUser(req.body);
  res.status(201).json(user);
});

Prompt :

Texte
Generate integration tests for POST /users using Jest and Supertest.

Cover:
- successful user creation
- missing email
- invalid email
- missing password
- duplicate email
- response must not include passwordHash

Use the existing test setup.

Tests possibles :

tsTest proposé
import request from "supertest";
import { app } from "../src/app";

describe("POST /users", () => {
  it("creates a user with valid input", async () => {
    const response = await request(app)
      .post("/users")
      .send({
        email: "alice@example.com",
        password: "StrongPassword123!",
        name: "Alice"
      });

    expect(response.status).toBe(201);
    expect(response.body.email).toBe("alice@example.com");
    expect(response.body).not.toHaveProperty("passwordHash");
  });

  it("returns 400 when email is missing", async () => {
    const response = await request(app)
      .post("/users")
      .send({
        password: "StrongPassword123!",
        name: "Alice"
      });

    expect(response.status).toBe(400);
  });

  it("returns 400 when email is invalid", async () => {
    const response = await request(app)
      .post("/users")
      .send({
        email: "not-an-email",
        password: "StrongPassword123!",
        name: "Alice"
      });

    expect(response.status).toBe(400);
  });
});

Ces trois tests ne couvrent pas encore tous les cas demandés dans le prompt (mot de passe manquant, e-mail en double). Vérifier que chaque cas demandé a son test fait partie de la relecture.

Exemple FastAPI

Prompt :

Texte
Generate integration tests for this FastAPI endpoint using pytest and TestClient.

Cover:
- valid request
- missing required field
- invalid payload
- unauthorized request
- forbidden request

Bon prompt pour tests d’intégration

Texte
Generate integration tests for [endpoint/workflow].

Use:
- [framework]
- existing test setup

Cover:
- successful flow
- validation errors
- authentication failure
- authorization failure
- dependency failure
- expected response format

Constraints:
- do not use real external services
- use test database or mocks where appropriate
- do not include real secrets

Attention

Les tests d’intégration peuvent être plus coûteux :

  • plus lents ;
  • plus fragiles ;
  • dépendants de l’environnement ;
  • dépendants de la base de données ;
  • dépendants de services externes.

Donc il faut souvent combiner :

Texte
tests unitaires + tests d’intégration + mocks/stubs adaptés

À retenir

Tests unitaires = comportement local.
Tests d’intégration = interaction entre composants.

10.6 Amélioration de la couverture de tests 📈

Définition

La couverture de tests mesure quelle partie du code est exécutée par les tests.

Elle peut inclure :

  • couverture de lignes ;
  • couverture de branches ;
  • couverture de fonctions ;
  • couverture de conditions.

Pourquoi Copilot est utile ?

Copilot peut :

  • identifier les branches non testées ;
  • proposer des tests pour les cas manquants ;
  • générer des tests pour erreurs ;
  • améliorer les assertions ;
  • suggérer des edge cases.

Exemple

Fonction :

ts
export function getShippingCost(country: string, total: number): number {
  if (total >= 100) {
    return 0;
  }

  if (country === "FR") {
    return 5;
  }

  if (country === "US") {
    return 10;
  }

  return 20;
}

Tests existants :

ts
it("returns free shipping for large orders", () => {
  expect(getShippingCost("FR", 100)).toBe(0);
});

Prompt :

Texte
Identify missing test cases for this function.
Focus on branch coverage and boundary values.

Un rapport de couverture dit quelles lignes ou quelles branches ne sont jamais exécutées ; il ne dit pas si les tests vérifient quelque chose. Pour que Copilot propose des tests utiles, donne-lui la fonction, les tests existants et les branches non couvertes du rapport, pas seulement « augmente la couverture ».

Copilot devrait proposer :

  • total exactly 100
  • total below 100
  • country FR
  • country US
  • unknown country
  • total 99.99

Tests :

tsTest proposé
describe("getShippingCost", () => {
  it("returns free shipping when total is exactly 100", () => {
    expect(getShippingCost("FR", 100)).toBe(0);
  });

  it("returns free shipping when total is greater than 100", () => {
    expect(getShippingCost("US", 150)).toBe(0);
  });

  it("returns 5 for France when total is below 100", () => {
    expect(getShippingCost("FR", 99)).toBe(5);
  });

  it("returns 10 for US when total is below 100", () => {
    expect(getShippingCost("US", 99)).toBe(10);
  });

  it("returns 20 for unknown countries when total is below 100", () => {
    expect(getShippingCost("DE", 99)).toBe(20);
  });
});

Bon prompt couverture

Texte
Review the selected function and existing tests.

Identify:
- uncovered branches
- missing edge cases
- missing error cases
- weak assertions

Then generate tests to improve coverage without modifying production code.

Piège d’examen

Une couverture élevée ne garantit pas que les tests sont bons.

Exemple mauvais test :

ts
it("runs", () => {
  getShippingCost("FR", 50);
});

Il exécute le code, mais ne vérifie rien.

Meilleur :

ts
expect(getShippingCost("FR", 50)).toBe(5);

À retenir

Couverture élevée ≠ qualité garantie.
Les assertions doivent vérifier le comportement.

À retenir pour le GH-300

  • Une fonction isolée se vérifie par des tests unitaires ; plusieurs composants qui collaborent, par des tests d’intégration. La portée du test suit ce qu’on veut prouver.
  • Un bon prompt de tests donne la cible, le contexte, les cas attendus et une contrainte : ne pas modifier le code de production, ne pas appeler de service réel.
  • Des tests générés se relisent : s’ils sont déduits du code, ils peuvent reproduire un bug. La référence est la règle métier.
  • La couverture montre ce qui n’est pas exécuté, pas ce qui est vérifié : une couverture élevée ne garantit pas un code correct.
  • Outil par tâche de test : Ask explore, Plan conçoit une stratégie multi-fichiers, Agent crée et exécute, ghost text étend un fichier qui montre déjà le motif.