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 :

Texte
Identify edge cases for this function.
Focus on invalid inputs, boundary values, empty values, null values, and security-sensitive cases.

Exemple

Fonction :

ts
export function getFirstItem<T>(items: T[]): T {
  return items[0];
}

Prompt :

Texte
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 :

ts
export function getFirstItem<T>(items: T[]): T {
  if (!items.length) {
    throw new Error("items must not be empty");
  }

  return items[0];
}

Tests :

tsTest proposé
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

Texte
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 :

py
assert calculate_discount(100, 20) == 80

ou en Jest :

ts
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 ❌

ts
expect(response.status).toBe(201);

C’est utile, mais incomplet.

Plus forte ✅

ts
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 :

Texte
Improve the assertions in this test.
Make sure it verifies the response body, status code, and that sensitive fields are not returned.

Test initial :

ts
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é :

tsTest proposé
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 :

py
with pytest.raises(ValueError, match="email must not be empty"):
    normalize_email("   ")

Jest :

ts
expect(() => normalizeEmail("   ")).toThrow("Email must not be empty");

Bon prompt assertions

Texte
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 :

ts
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 :

Texte
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 email

Tests possibles :

tsTest proposé
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 :

Texte
Generate pytest tests for this service.
Mock the external payment client.
Cover success, declined payment, and network error.

Exemple :

py
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.