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 test | Outil |
|---|---|
| Explorer les cas à couvrir ou choisir un framework, sans rien modifier | Ask |
| Concevoir une stratégie de tests sur plusieurs fichiers avant d’écrire du code | Plan |
| Créer le projet de tests, écrire, exécuter et corriger en boucle | Agent |
| Ajouter des cas à un fichier de tests qui montre déjà le motif à suivre | Ghost text (suggestions inline) |
| Corriger un test qui échoue | Fix 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 :
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 :
Write tests.Bon prompt :
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 :
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 :
export function normalizeEmail(email: string): string {
const normalizedEmail = email.trim().toLowerCase();
if (!normalizedEmail) {
throw new Error("Email must not be empty");
}
return normalizedEmail;
}Prompt :
Generate Jest unit tests for normalizeEmail.
Cover:
- trimming spaces
- lowercasing
- already normalized email
- empty string
- whitespace-only stringTests possibles :
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
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 casesPiè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 test | Objectif |
|---|---|
| Test unitaire | Vérifier une unité isolée |
| Test d’intégration | Vérifier l’interaction entre plusieurs composants |
Exemple Express API
Endpoint conceptuel :
app.post("/users", validateUserInput, async (req, res) => {
const user = await userService.createUser(req.body);
res.status(201).json(user);
});Prompt :
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 :
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 :
Generate integration tests for this FastAPI endpoint using pytest and TestClient.
Cover:
- valid request
- missing required field
- invalid payload
- unauthorized request
- forbidden requestBon prompt pour tests d’intégration
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 secretsAttention
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 :
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 :
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 :
it("returns free shipping for large orders", () => {
expect(getShippingCost("FR", 100)).toBe(0);
});Prompt :
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 :
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
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 :
it("runs", () => {
getShippingCost("FR", 50);
});Il exécute le code, mais ne vérifie rien.
Meilleur :
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.
Pratique : Quel test demander à Copilot ?
Chaque situation décrit une tâche de test. Choisis la demande à faire à Copilot, ou l’outil à utiliser : le type de test et l’outil dépendent de ce qu’on veut vérifier. Rien n’est exécuté : tu raisonnes, puis tu vois pourquoi les autres propositions ne conviennent pas.
Chargement de la question…
Questions de la leçon
Chargement de la question…
Pour finir
À retenir
- 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.
Objectif GH-300
- Tests unitaires et d’intégration (Generate unit and integration tests)
Achèvement
Il reste à faire : , .