Les tests unitaires

Ludovic Deneuville

Les Tests

Source : CommitStrip

Source : CommitStrip

Pourquoi tester ?

  • Crash test de voiture
  • Test de conformité
  • Test qualité en industrie
  • Test statistique

En informatique

  • Pour vérifier que votre programme fonctionne
  • Pour détecter des erreurs
  • Pour éviter les régressions (quand vous modifiez du code)

Note

Aucun test n’est parfait, mais cela permet quand même d’écarter de nombreuses erreurs.

Définition

Un test ressemble à une expérience scientifique.

Il examine une hypothèse exprimée en fonction de trois éléments :

  • les données en entrée
  • l’objet à tester
  • les résultats attendus

Cet examen est effectué sous conditions contrôlées pour pouvoir tirer des conclusions et, dans l’idéal, être reproduit.

Couverture de test

  • Pourcentage de fonctions testées
  • Indicateur de qualité
  • Tendance, plutôt qu’une valeur fiable

Préférez faire peu de tests, mais des tests utiles !

Les types de tests

Il existe de très nombreux tests différents, voici les principaux :

  • Test unitaire : teste une fonction pour vérifier son bon fonctionnement
  • Test fonctionnel : teste les cas d’utilisation du logiciel
  • Test de charge : évaluent la capacité d’un système à gérer un volume élevé de données ou de transactions
  • Test d’intégration : vérifie que différents composants ou modules d’un système interagissent correctement ensemble
  • Test d’intrusion : vérifie la sécurité du logiciel en recherchant des vulnérabilités et en simulant des attaques potentielles

Les tests unitaires

Nous allons utiliser le package pytest pour réaliser nos tests en Python.

Un bon test unitaire

  • Teste une seule fonctionnalité
  • Isolé
  • Reproductible
  • Déterministe

Méthode à tester

operations_mathematiques.py
class OperationsMathematiques:
    """Opérations Mathématiques"""
    def diviser_cinq_par(self, nombre) -> float:
        """Divise le nombre 5 par un nombre donné.
        Parameters
        ----------
        nombre : float or int
            Le nombre par lequel 5 sera divisé.
        Returns
        -------
        float or None
            Le résultat de la division de 5 par le nombre donné.
            Si le nombre est égal à 0, la méthode retourne None.
        """
        if nombre != 0:
            return 5 / nombre
        else:
            return None

Classe de test

Créons une classe de test.

Pour tester le cas nominal, nous :

  • choisissons un nombre en entrée
  • appelons la méthode diviser_cinq_par()
  • vérifions que la valeur retournée est égale à la valeur attendue

Cas Nominal

test_operations_mathematiques.py
import pytest
from mathematiques.operations_mathematiques import OperationsMathematiques

class TestOperationsMathematiques():
    def test_diviser_cinq_par_nombre_non_nul(self):
        # GIVEN
        nombre = 2

        # WHEN
        resultat = OperationsMathematiques().diviser_cinq_par(nombre)

        # THEN
        assert resultat == 2.5

Autres cas

Mais ce n’est pas suffisant !

  • La méthode a également un autre retour possible : None
  • Il faut aussi tester ce cas
test_operations_mathematiques.py
    def test_diviser_cinq_par_zero(self):
        # GIVEN
        nombre = 0

        # WHEN
        resultat = OperationsMathematiques().diviser_cinq_par(nombre)

        # THEN
        assert resultat is None

Et si…

Nous appelons la méthode avec ce paramètre : diviser_cinq_par("a") ?

Vous pouvez aussi écrire un test pour vérifier que votre méthode renvoie bien une exception TypeError dans ce cas.

test_operations_mathematiques.py
    def test_diviser_cinq_string(self):
        # GIVEN
        nombre = "a"

        # WHEN / THEN
        with pytest.raises(TypeError):
            OperationsMathematiques().diviser_cinq_par(nombre)

Ce qu’il faut retenir

Les tests unitaires

  • vérifient qu’une méthode fait ce qu’elle doit faire
  • il faut tester les cas nominaux, mais également les cas à la marge et les erreurs
  • un test unitaire teste UNE et UNE seule chose
  • il faut autant de tests unitaires que de retours possibles

Mock

  • Objet simulé qui remplace un composant réel lors des tests
  • Permet de tester une unité de code en isolant ses dépendances externes
  • Simuler des scénarios complexes comme les erreurs de réseau

Mock - exemple

joueur_service.py
class JoueurService:
    def creer(self, pseudo, mdp, age, mail, fan_pokemon) -> Joueur:
        nouveau_joueur = Joueur(
            pseudo=pseudo,
            mdp=hash_password(mdp, pseudo),
            age=age,
            mail=mail,
            fan_pokemon=fan_pokemon,
        )
        creation_ok = JoueurDao().creer(nouveau_joueur)
        if creation_ok:
            return nouveau_joueur
        else 
            return None

Mock - exemple

test_joueur_service.py
from unittest.mock import MagicMock

def test_creer_ok():
    """ "Création de Joueur réussie"""

    # GIVEN
    pseudo, mdp, age, mail, fan_pokemon = "jp", "1234", 15, "z@mail.oo", True
    JoueurDao().creer = MagicMock(return_value=True)

    # WHEN
    joueur = JoueurService().creer(pseudo, mdp, age, mail, fan_pokemon)

    # THEN
    assert joueur.pseudo == pseudo

Les Test Driven Development (TDD)

Quand tester ?

Au début !

Tip

Plus on teste tôt ➡️ plus les tests sont efficaces et peu coûteux !

Test Driven Development

La meilleure pratique :

  1. Créer les tests
  2. Coder la fonction

Logique

Cela paraît un peu étrange, mais en fait pas tant que ça.

Lorsque vous codez une fonction, vous savez avant de commencer :

  • quels seront les paramètres en entrée
  • quels résultats vous attendez en sortie
  • donc vous savez déjà quoi tester !

Pratique du TDD

  • ✅ Amélioration de la qualité du code
  • ✅ Réduction des bugs
  • ❌ puis ✅ Temps
  • ❌ Maintenance des tests

Important

Avantages >>> Incovénients