10 Tests, sécurité et qualité du code
Edge cases, assertions et mocks
10.3 Identification des edge cases 🧠
Définition
Un edge case est un cas limite ou inhabituel qui peut casser le code si on ne l’a pas prévu.
Exemples :
- liste vide ;
- null ou undefined ;
- chaîne vide ;
- très grande valeur ;
- valeur négative ;
- date invalide ;
- timezone ;
- doublons ;
- permissions insuffisantes ;
- service externe indisponible ;
- réponse API inattendue.
Pourquoi Copilot est utile ?
Copilot peut t’aider à brainstormer des cas que tu n’as pas pensés.
Prompt utile :
Identify edge cases for this function.
Focus on invalid inputs, boundary values, empty values, null values, and security-sensitive cases.Exemple
Fonction :
export function getFirstItem<T>(items: T[]): T {
return items[0];
}Prompt :
Identify edge cases for this function and suggest tests.Copilot peut proposer :
- empty array
- array with one item
- array with multiple items
- null or undefined input
- sparse array
Amélioration possible
Si tu veux rendre la fonction plus sûre :
export function getFirstItem<T>(items: T[]): T {
if (!items.length) {
throw new Error("items must not be empty");
}
return items[0];
}Tests :
describe("getFirstItem", () => {
it("returns the first item", () => {
expect(getFirstItem([1, 2, 3])).toBe(1);
});
it("throws when the array is empty", () => {
expect(() => getFirstItem([])).toThrow("items must not be empty");
});
});Edge cases par catégorie
Données
- null
- undefined
- empty string
- whitespace string
- empty array
- duplicate values
- malformed object
Nombres
- zero
- negative values
- very large numbers
- decimals
- NaN
- Infinity
Dates
- invalid date
- timezone differences
- daylight saving time
- leap year
- start/end of month
Sécurité
- injection payload
- missing auth
- insufficient permissions
- expired token
- malformed token
- sensitive data in logs
API
- missing fields
- unknown fields
- invalid content type
- rate limit
- timeout
- dependency failure
Bon prompt edge cases
Identify edge cases for the selected function.
Group them by:
- input validation
- boundary values
- business rules
- security risks
- dependency failures
Then suggest unit tests for the top priority cases.À retenir
Copilot est très utile pour trouver des edge cases, mais c’est au développeur de décider lesquels sont vraiment pertinents métier.
Quand tu demandes des cas à Copilot, pense à cinq familles : le chemin nominal, les valeurs limites, les entrées invalides, les valeurs absentes (null, vide) et les chemins d’échec (dépendance indisponible, délai dépassé). Aucune connaissance d’un langage précis n’est nécessaire pour les demander.
10.4 Écriture d’assertions ✅
Définition
Une assertion vérifie qu’un résultat réel correspond au résultat attendu.
Exemple :
assert calculate_discount(100, 20) == 80ou en Jest :
expect(calculateDiscount(100, 20)).toBe(80);Peu importe la syntaxe (assert, expect…) : la question utile est « quel comportement ce test doit-il vérifier ? ». Un résultat attendu, une erreur attendue, un effet de bord attendu, une donnée qui ne doit pas apparaître.
Assertions fortes vs faibles
Une assertion faible vérifie trop peu.
Faible ❌
expect(response.status).toBe(201);C’est utile, mais incomplet.
Plus forte ✅
expect(response.status).toBe(201);
expect(response.body.email).toBe("alice@example.com");
expect(response.body).not.toHaveProperty("passwordHash");
expect(response.body.id).toBeDefined();Exemple API
Prompt :
Improve the assertions in this test.
Make sure it verifies the response body, status code, and that sensitive fields are not returned.Test initial :
it("creates a user", async () => {
const response = await request(app)
.post("/users")
.send({
email: "alice@example.com",
password: "StrongPassword123!"
});
expect(response.status).toBe(201);
});Test amélioré :
it("creates a user without exposing passwordHash", async () => {
const response = await request(app)
.post("/users")
.send({
email: "alice@example.com",
password: "StrongPassword123!"
});
expect(response.status).toBe(201);
expect(response.body.id).toBeDefined();
expect(response.body.email).toBe("alice@example.com");
expect(response.body).not.toHaveProperty("password");
expect(response.body).not.toHaveProperty("passwordHash");
});Assertions pour erreurs
Python :
with pytest.raises(ValueError, match="email must not be empty"):
normalize_email(" ")Jest :
expect(() => normalizeEmail(" ")).toThrow("Email must not be empty");Bon prompt assertions
Review these tests and improve the assertions.
Focus on:
- verifying status codes
- verifying response body
- checking error messages
- ensuring sensitive fields are not returned
- avoiding overly broad assertionsÀ retenir
Un test utile ne vérifie pas seulement que “ça passe”.
Il vérifie le comportement attendu avec précision.
10.5 Utilisation de mocks 🎭
Définition
Un mock est un faux composant utilisé dans un test pour remplacer une dépendance réelle.
On utilise des mocks pour éviter d’appeler :
- une vraie base de données ;
- une API externe ;
- un service de paiement ;
- un service email ;
- un bucket cloud ;
- un système de fichiers réel ;
- un service lent ou instable.
Concrètement, un mock sert à trois choses : isoler l’unité testée, contrôler un effet de bord (un e-mail qui part, un paiement qui se débite) et rendre le test déterministe (pas de réseau, d’horloge ni de hasard). C’est un concept à reconnaître : la syntaxe d’une bibliothèque de mocks n’est pas un sujet d’examen.
Exemple TypeScript
Service :
export class PasswordResetService {
constructor(
private readonly userRepository: UserRepository,
private readonly emailService: EmailService
) {}
async requestReset(email: string): Promise<void> {
const user = await this.userRepository.findByEmail(email);
if (!user || !user.active) {
return;
}
await this.emailService.sendPasswordResetEmail(user.email);
}
}Prompt :
Generate Jest unit tests for PasswordResetService.
Use mocks for:
- userRepository
- emailService
Cover:
- active user receives reset email
- unknown user does not receive email
- inactive user does not receive emailTests possibles :
describe("PasswordResetService", () => {
const userRepository = {
findByEmail: jest.fn()
};
const emailService = {
sendPasswordResetEmail: jest.fn()
};
let service: PasswordResetService;
beforeEach(() => {
jest.clearAllMocks();
service = new PasswordResetService(userRepository, emailService);
});
it("sends reset email for active user", async () => {
userRepository.findByEmail.mockResolvedValue({
email: "alice@example.com",
active: true
});
await service.requestReset("alice@example.com");
expect(emailService.sendPasswordResetEmail).toHaveBeenCalledWith("alice@example.com");
});
it("does not send reset email for unknown user", async () => {
userRepository.findByEmail.mockResolvedValue(null);
await service.requestReset("missing@example.com");
expect(emailService.sendPasswordResetEmail).not.toHaveBeenCalled();
});
it("does not send reset email for inactive user", async () => {
userRepository.findByEmail.mockResolvedValue({
email: "inactive@example.com",
active: false
});
await service.requestReset("inactive@example.com");
expect(emailService.sendPasswordResetEmail).not.toHaveBeenCalled();
});
});Exemple simplifié : dans un projet TypeScript strict, les faux objets sont typés comme les dépendances qu’ils remplacent.
Exemple Python avec unittest.mock
Prompt :
Generate pytest tests for this service.
Mock the external payment client.
Cover success, declined payment, and network error.Exemple :
from unittest.mock import Mock
import pytest
def test_process_payment_success():
payment_client = Mock()
payment_client.charge.return_value = {"status": "paid"}
service = PaymentService(payment_client)
result = service.process_payment(amount=100)
assert result["status"] == "paid"
payment_client.charge.assert_called_once_with(amount=100)Bonnes pratiques mocks
Utilise des mocks pour :
- isoler l’unité testée
- éviter les appels externes
- simuler erreurs et timeouts
- tester les branches difficiles
- rendre les tests reproductibles (pas de réseau, d’horloge ni de hasard)
Évite de trop mocker :
- si le test ne vérifie plus le vrai comportement
- si tu mocks tout le système
- si le mock ne ressemble pas à la dépendance réelle
À retenir
Mocker, c’est isoler.
Mais trop mocker peut créer des tests irréalistes.
À retenir pour le GH-300
- Demande à Copilot les cas nominaux, limites, invalides, absents et d’échec ; le développeur choisit ceux qui comptent pour le métier.
- Une assertion décrit le comportement à vérifier (résultat, erreur, effet de bord, donnée absente), pas une syntaxe.
- Un mock isole une dépendance externe, contrôle ses effets et rend le test reproductible ; simuler l’unité testée rend le test inutile.
Pratique : Que manque-t-il à ces tests ?
Copilot a proposé des tests. Pour chaque extrait, repère ce qui manque ou ce qu’il faut corriger : un cas limite, une assertion plus précise, un mock mal placé. La syntaxe n’est pas évaluée et aucun code n’est exécuté : tu raisonnes sur ce que le test doit vérifier.
Chargement de la question…
Questions de la leçon
Chargement de la question…
Pour finir
À retenir
- Demande à Copilot les cas nominaux, limites, invalides, absents et d’échec ; le développeur choisit ceux qui comptent pour le métier.
- Une assertion décrit le comportement à vérifier (résultat, erreur, effet de bord, donnée absente), pas une syntaxe.
- Un mock isole une dépendance externe, contrôle ses effets et rend le test reproductible ; simuler l’unité testée rend le test inutile.
Objectif GH-300
- Cas limites et assertions (Identify edge cases and write assertions)
Achèvement
Il reste à faire : , .