# Tests unitaires

## Qu'est-ce qu'un test unitaire ?

Les tests unitaires sont des tests automatisés permettant d'assurer le bon fonctionnent de parties spécifiques d'un programme (des fonctions) de manière isolée.

Les tests unitaires peuvent être exécutés automatiquement chaque fois que des modifications sont apportées au code. Cela garantit une validation rapide après chaque modification.

{% hint style="info" %}
**En résumé, un test unitaire est une fonction qui teste une autre fonction. À la place de faire des tests manuellement, on les automatise à l'aide de fonctions à exécuter.**&#x20;
{% endhint %}

## Qu'est-ce qu'on veut vérifier dans un test unitaire?

Dans ce cours, le minimum à vérifier serait les points suivants : &#x20;

* Vérifier la valeur et le type de retour d’une fonction.
* Vérifier le contenu d’une liste (valeur, type, nombre d'éléments).
* Vérifier les cas limites (ex. dépassement de capacité, division par zéro, etc.).

D'autres vérifications existent pour assurer le fonctionnement parfait d'une fonction. Vous aurez l'occasion de les découvrir au fil des exercices (si l'occasion se présente) et dans vos prochains cours de programmation.

## Structure d'une fonction de test unitaire : AAA

La méthodologie utilisée pour structurer un test unitaire est AAA pour "*Arrange*", "*Act*", et "*Assert*". Elle est souvent utilisée pour structurer les tests unitaires.

1. **Arrange (Préparer) :**&#x20;

   Mettre en place toutes les conditions préalables nécessaires pour exécuter le test.&#x20;

   * &#x20;<mark style="background-color:green;">Préparation et l'initialisation des données.</mark>
   * Configuration de l'environnement.
   * Instanciation d'objets.
   * etc.
2. **Act (Agir) :**

   Exécuter l'action ou le comportement que vous souhaitez tester.&#x20;

   * <mark style="background-color:green;">Appel de la fonction à tester</mark>,&#x20;
   * manipulation d'objets,
   * ou toute autre opération qu'on veut évaluer.
3. **Assert (Vérifier) :**

   Vérifier que le résultat de l'action effectuée dans la phase "*Act*" est conforme aux attentes.&#x20;

   * <mark style="background-color:green;">Si le résultat est celui attendu, le test passe ; sinon, le test échoue.</mark>

## Méthode de test

Les tests unitaires se font à l'aide de modules intégrés ou tiers. Le module que nous allons utiliser est pytest.

### Tests unitaires avec pytest

&#x20;[Voir les étapes décrites ici pour installer le module pytest](https://cours-5.gitbook.io/dalicia-420-1g2-hu-logique-de-programmation/les-modules#installation-de-modules-tiers).

* Un fichier de test est un fichier python (.py) dont le nom est préfixé par ***test\_*** suivi du nom du fichier python à tester. Exemple : `test_fonctions_a_tester.py` est le fichier dans lequel se trouve les tests unitaires du programme python `fonctions_a_tester.py`.
* Le nom de la fonction de test unitaire doit être préfixée par ***test\_***. Par soucis de clarté, on fait suivre le préfixe ***test\_*** par le nom de la fonction à tester (ou un nom similaire indiquant le test précis).
* Afin d'utiliser certaines fonctionnalités (décorateurs) de pytest, on doit importer pytest: `import pytest`.

{% hint style="danger" %}
Le nom de la fonction doit obligatoirement commencer par le mot clé *`test_.`* pour qu'elle soit reconnue comme étant un test unitaire.
{% endhint %}

{% hint style="success" %}
**Bonne pratique :**&#x20;

* On créé un projet de test pour chaque projet python à tester.
* Dans un projet de test, on créé un fichier de test par fichier .py du projet à tester.
  {% endhint %}

Les exemples qui vont suivre sont basés sur la structure suivante contenant le code pour les tests unitaires : &#x20;

<figure><img src="https://700524925-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fn5YnWU89G98lz0ROIcGu%2Fuploads%2FBduMUXnSvGWlyxlzmiVB%2Fimage.png?alt=media&#x26;token=99260da5-a077-45cf-b89e-714002fcd5bb" alt=""><figcaption></figcaption></figure>

`fonction_a_tester.py` est le fichier du programme dans lequel se trouve les fonctions à tester.

`test_fonction_a_tester.py` est le fichier dans lequel se trouve les fonction de test du fichier `fonction_a_tester.py`.

### Syntaxe des vérifications `assert`&#x20;

```python
assert condition[, "message d'erreur optionnel"]
```

* ***condition*** : la condition qui exprime le résultat attendu est évaluée. Si le résultat est vrai, le test passe ; sinon, le test échoue.
* Le message d'erreur est optionnel.

#### Exemples

<pre class="language-python"><code class="lang-python"><strong>assert resultat == resultat_attendu
</strong>assert resultat in liste_resultats_attendus
assert min_resultat_attendu &#x3C; resultat  &#x3C; max_resultat_attendu 
...
</code></pre>

### Exemple d'un test unitaire&#x20;

**Fonction à tester :**&#x20;

{% code title="fonction\_a\_tester.py" fullWidth="false" %}

```python
def division_entiere_1(n, m):
    return n//m
```

{% endcode %}

**Exemple d'un test unitaire de base  pour la fonction précédente :**&#x20;

[Voir la structure AAA pour plus de détails sur # Arrange # Act et # Assert.](#structure-dune-fonction-de-test-unitaire-aaa)

{% code title="test\_fonction\_a\_tester.py" %}

```python
import pytest
import Tests_unitaires.projet_pour_tests_unitaires.fonctions_a_tester as fonctions_a_tester

def test_division_entiere_1():
    # Arrange
    n = 6
    m = 2
    resultat_attendu = 3
    # Act
    resultat_division = fonctions_a_tester.division_entiere_1(n, m)

    # Assert
    assert resultat_division == resultat_attendu

```

{% endcode %}

* *Arrange* : on définit les données de test `n=6` et `m=2`.
* *Act* : on appelle la fonction `division_entiere_1` (qu'on veut tester).
* *Assert* : on vérifie si le résultat de la division de `n=6` sur `m=2` est bien égal à 3.

{% hint style="danger" %}
**Isolation** : une unité spécifique de code doit être testée **seule** pour s'assurer qu'elle fonctionne indépendamment des autres parties du programme.

* On teste une seule fonction à la fois (une fonction de test teste une seule fonction). Il peut y avoir plusieurs fonctions de test pour une seule fonction à tester.
* On n'appelle pas d'autres fonctions qui ont besoin d'être testées (autre que celle qu'on veut tester).&#x20;
  {% endhint %}

{% hint style="danger" %}
**Précision :**&#x20;

* Les tests unitaires doivent couvrir divers scénarios de données et vérifier que l'unité testée produit les résultats attendus dans toutes les situations.
* <mark style="color:red;">Pour une fonction à tester, on regroupe les tests du même type dans la même fonction de test.</mark>
  {% endhint %}

## Les données de test

Avant d'écrire un test unitaire, on doit préparer un plan de tests contenant les données à tester. Ce données sont mentionnées au dessus d'un test unitaire :&#x20;

**Syntaxe :**&#x20;

```python
@pytest.mark.parametrize("p1, p2, ..., resultat_attendu", [
    (d1_p1, d1_p2, ..., resultat_attendu1),
    (d2_p1, d2_p2, ..., resultat_attendu2),
    ...,
    (dN_p1, dN_p2, ..., resultat_attenduN),
])
def test_fonction(p1, p2, ..., resultat_attendu):
    # Instructions qui utilisent les données des 
    # paramètres dans les sections Arrange/Act/Assert
```

**Exemple :**&#x20;

<pre class="language-python"><code class="lang-python"><strong># Arrange
</strong>@pytest.mark.parametrize("n, m, resultat_attendu", [
    (10, 2, 5),
    (-10, 2, -5),
    (3, 2, 1),
    (1000000, 10, 100000),
    (0, 5, 0),
    (1, 3, 0)
])
def test_division_entiere_1(n, m, resultat_attendu):
    # Arrange --> c'est le décorateur @pytest.mark.parametrize au dessus de la définition de la fonction qui prépare les données de tests.

    # Act
    resultat = fonctions_a_tester.division_entiere_1(n, m)

    # Assert
    assert isinstance(resultat, int)
    assert resultat == resultat_attendu
</code></pre>

## Vérifications (Assert) minimales à faire (selon le type du résultat retourné)

### Vérifications pour les types simples&#x20;

<table><thead><tr><th>La vérification</th><th>Syntaxe</th><th>Exemple</th></tr></thead><tbody><tr><td><h4>Vérifier que le résultat retourné par la fonction a tester a le bon type.</h4></td><td><pre class="language-python"><code class="lang-python">assert isinstance(&#x3C;resultat>, &#x3C;type>)
</code></pre></td><td><pre class="language-python"><code class="lang-python">assert isinstance(resultat, int)
</code></pre></td></tr><tr><td><h4>Vérifier que le résultat est celui attendu.</h4></td><td><pre class="language-python"><code class="lang-python">assert &#x3C;condition>
</code></pre></td><td><pre class="language-python"><code class="lang-python">assert resultat == resultat_attendu
assert resultat in liste_resultats_attendus
assert min_resultat_attendu &#x3C; resultat  &#x3C; max_resultat_attendu 
</code></pre></td></tr></tbody></table>

### Vérifications pour les résultats de type liste&#x20;

<table><thead><tr><th>La vérification</th><th>Exemple</th></tr></thead><tbody><tr><td><strong>Vérifier que le résultat est bien de type list</strong></td><td><pre class="language-python"><code class="lang-python"><strong>assert isinstance(&#x3C;resultat>, list)
</strong></code></pre></td></tr><tr><td><strong>Vérifier que la longueur de la liste est celle attendue</strong></td><td><p>Pour vérifier l'ajout d'un élément dans une liste.</p><ul><li>La variable <code>longueur_init</code> est la longueur initiale de la liste avant l'ajout de l'élément.</li><li>La variable <code>resultat</code> contient la liste retournée par l'appel à la fonction.</li></ul><pre class="language-python"><code class="lang-python">longueur_resultat = len(resultat)
assert longueur_resultat  == longueur_init + 1
</code></pre></td></tr><tr><td><strong>Vérifier que chaque élément de la liste a le bon type.</strong></td><td><pre class="language-python"><code class="lang-python">for e in resultat:
    assert isinstance(e, int)
</code></pre><pre class="language-python"><code class="lang-python">for e in resultat:
    assert isinstance(e, int) or isinstance(e, float)
</code></pre><pre class="language-python"><code class="lang-python">assert all(isinstance(e, float) for e in resultat)
</code></pre></td></tr><tr><td><strong>À l'ajout d'un élément à une liste : vérifier que l'élément ajouté est bien à la bonne place.</strong></td><td><pre class="language-python"><code class="lang-python">assert element_ajoute in resultat
</code></pre><pre class="language-python"><code class="lang-python">assert resultat[position] == element_ajoute
</code></pre></td></tr><tr><td><strong>À la suppression d'un élément dans une liste : L'élément supprimé n'existe plus dans la liste (<code>not in</code>).</strong></td><td><p></p><pre class="language-python"><code class="lang-python">assert element_supprime not in resultat
</code></pre></td></tr><tr><td><strong>À la modification d'un élément dans une liste : - la modification de l'élément a bien été faite correctement.</strong><br><strong>- L'ancien élément avant la modification n'existe plus dans la liste.</strong></td><td><pre class="language-python"><code class="lang-python">assert resultat[position] == element_modifie
</code></pre><pre class="language-python"><code class="lang-python">assert element_avant_modif not in resultat
</code></pre></td></tr></tbody></table>

### Vérifications pour les exceptions

<table><thead><tr><th>La vérification</th><th>Syntaxe</th><th>Exemple</th></tr></thead><tbody><tr><td><strong>Vérifier qu'une exception est gérée</strong></td><td><pre class="language-python"><code class="lang-python">@pytest.mark.xfail(raises=&#x3C;type_exception>)
def test_fonction():

```
fonctions_a_tester(...)
```

</code></pre></td><td><pre class="language-python"><code class="lang-python">@pytest.mark.xfail(raises=TypeError)
def test\_fonction():

```
fonctions_a_tester(...)
```

</code></pre></td></tr></tbody></table>

## Exemples&#x20;

{% code title="fonction\_a\_tester.py" %}

```python
def division_entiere(n, m):
    try:
        return n//m
    except ZeroDivisionError:
        return None
```

{% endcode %}

<pre class="language-python"><code class="lang-python">
@pytest.mark.parametrize("a, b, resultat_attendu", [
    (10, 2, 5),
    (-10, 2, -5),
    (3, 2, 1),
    (1000000, 10, 100000),
    (0, 5, 0),
    (1, 3, 0)
])
def test_division_entiere(a, b, resultat_attendu):
    # Arrange
    # Act
    resultat = demo.division_entiere(a, b)

    # Assert
    assert isinstance(resultat, int)
    assert resultat == resultat_attendu

# Ici, l'exception ZeroDivisionError est gérée, ce teste va donc passer.
@pytest.mark.xfail(raises=ZeroDivisionError)
def test_exception_zero_division_entiere():
    resultat = demo.division_entiere(5, 0)
    resultat_attendu = 999999999
    assert resultat == resultat_attendu
    
# Ici, l'exception TypeError n'est pas gérée, ce teste NE va donc PAS passer.
@pytest.mark.xfail(raises=TypeError)
def test_exception_TypeError_division_entiere():
    resultat = demo.division_entiere("10", "5")
<strong>    resultat_attendu = None
</strong>    assert resultat == resultat_attendu

</code></pre>

## Exercices guidés

Pour chaque fonction :&#x20;

* Faites le plan de test : les données d'entrée, le résultat attendu
* Listez les vérifications (assert) à faire.
* Créez votre teste unitaire sur la base des deux points précédents.

#### Fonction 1 : generer\_adresses\_courriel

```python
def generer_adresses_courriel(prenom:str, nom:str, domaine:str)->str:
    """
    Fonction qui génère une adresse courriel au format
    [nom].[prenom]@[domaine] à partir d'un prenom, nom et domaine.
    :param prenom: Le prenom de la personne.
    :param nom: Le nom de la personne.
    :param domaine: Le nom de domaine pour l'adresse courriel
    :return: L'adresse courriel au format [nom].[prenom]@[domaine]
             tout en lettres minuscules.
    """

    adresse = f"{prenom.lower()}.{nom.lower()}@{domaine}"

    return adresse
```

#### Test unitaire de la fonction generer\_adresses\_courriel

{% code title="Test unitaire de la fonction generer\_adresses\_courriel " %}

```python
# Arrange
@pytest.mark.parametrize("prenom, nom, domaine, resultat_attendu", [
    ("Alice", "Smith", "company.org", "alice.smith@company.org"),
    ("bob", "Johnson", "webmail.net", "bob.johnson@webmail.net"),
    ("Eva", "lee", "personal.info", "eva.lee@personal.info")
    # Ajoutez d'autres cas de test si nécessaire
])
def test_generer_adresses_courriel(prenom, nom, domaine, resultat_attendu):
    # Act
    resultat = fonctions_a_tester.generer_adresses_courriel(prenom, nom, domaine)

    # Assert
    assert isinstance(resultat, str)
    assert resultat == resultat_attendu
```

{% endcode %}

#### Fonction 2 : ajouter\_tache

<mark style="color:red;">\[...cet exemple n'est pas très pertinent car la méthode .insert(...) sur les listes est écrite correctement et fait déjà des vérifications mais l'idée est de réfléchir à ce qu'il faudrait vérifier dans un test unitaire...]</mark>

```python
def ajouter_tache(liste_taches, tache, priorite):
    """
    Ajoute une tâche à une liste de tâches selon sa priorité dans la liste.
    :param liste_taches: la liste des tâches existante.
    :param tache: la tâche à ajouter dans la liste.
    :param priorite: La position à laquelle on ajoute la tâche.
    :return: retourne la liste incluant la tâche ajoutée.
    """
    liste_taches.insert(priorite, tache)
    return liste_taches
```

#### Tests unitaires de la fonction ajouter\_tache

{% code title="Test unitaire de la fonction ajouter\_tache" %}

```python
# Arrange
@pytest.mark.parametrize("liste_taches, tache, priorite, resultat_attendu", [
    (["Faire les courses"], "Répondre aux courriels", 0, ["Répondre aux courriels", "Faire les courses"]),
    (["Travailler sur le projet", "Préparer le dîner", "Lire un livre"], "Faire du sport", 2, ["Travailler sur le projet", "Préparer le dîner", "Faire du sport", "Lire un livre"]),
    ([], "Commencer le rapport", 0, ["Commencer le rapport"]),
    (["Réviser pour l'examen"], "Prendre une pause", 1, ["Réviser pour l'examen", "Prendre une pause"]),
    # Ajoutez d'autres cas de test si nécessaire
])
def test_ajouter_tache(liste_taches, tache, priorite, resultat_attendu):

    # Arrange
    longueur_init = len(liste_taches)

    # Act
    resultat = fonctions_a_tester.ajouter_tache(liste_taches, tache, priorite)

    # Assert
    assert len(liste_taches) == longueur_init + 1
    assert isinstance(resultat, list)
    assert all(isinstance(tache, str) for tache in resultat)
    assert resultat == resultat_attendu
    assert resultat[priorite] == tache
```

{% endcode %}

{% embed url="<https://docs.pytest.org/en/7.4.x/>" %}
