Exercice

Effectué

Dans cet exercice, vous utilisez Pytest pour tester une fonction. Ensuite, vous trouvez et corrigez quelques problèmes potentiels avec la fonction qui entraîne l’échec de tests. Il est essentiel d’examiner les échecs et d’utiliser les rapports d’erreurs complets de Pytest pour identifier et corriger les bogues ou tests problématiques dans le code de production.

Pour cet exercice, nous utilisons une fonction appelée admin_command() qui accepte une commande système comme entrée, et éventuellement la préfixer avec l’outil sudo. La fonction présente un bogue que vous découvrez en écrivant des tests.

Étape 1 - Ajouter un fichier avec des tests pour cet exercice

  1. Créez un fichier de test à l’aide des conventions de nom de fichier de Python pour les fichiers de test. Nommez le fichier de test test_exercise.py et ajoutez le code suivant :

    def admin_command(command, sudo=True):
        """
        Prefix a command with `sudo` unless it is explicitly not needed. Expects
        `command` to be a list.
        """
        if sudo:
            ["sudo"] + command
        return command
    

    La fonction admin_command() prend une liste comme entrée en utilisant l’argument command et peut éventuellement préfixer la liste avec sudo. Si l’argument de mot clé sudo est défini sur False, il retourne la même commande donnée comme entrée.

  2. Dans le même fichier, ajoutez les tests pour la fonction admin_command(). Les tests utilisent une méthode d’assistance qui retourne un exemple de commande :

    class TestAdminCommand:
    
    def command(self):
        return ["ps", "aux"]
    
    def test_no_sudo(self):
        result = admin_command(self.command(), sudo=False)
        assert result == self.command()
    
    def test_sudo(self):
        result = admin_command(self.command(), sudo=True)
        expected = ["sudo"] + self.command()
        assert result == expected
    

Notes

Il n’est pas courant d’avoir des tests dans le même fichier que le vrai code. Pour faire simple, les exemples de cet exercice ont du vrai code dans le même fichier. Dans les vrais projets Python, les tests sont généralement séparés par des fichiers et des répertoires du code qu’ils testent.

Étape 2 : exécuter les tests et identifier la défaillance

Maintenant que le fichier de test a une fonction à tester et quelques tests pour vérifier son comportement, il est temps d’exécuter les tests et de traiter les échecs.

  • Exécutez le fichier avec Python :

    $ pytest test_exercise.py
    

    L’exécution doit se terminer avec un test réussi et un échec. La sortie d’échec doit ressembler à la sortie suivante :

    =================================== FAILURES ===================================
    __________________________ TestAdminCommand.test_sudo __________________________
    
    self = <test_exercise.TestAdminCommand object at 0x10634c2e0>
    
        def test_sudo(self):
            result = admin_command(self.command(), sudo=True)
            expected = ["sudo"] + self.command()
    >       assert result == expected
    E       AssertionError: assert ['ps', 'aux'] == ['sudo', 'ps', 'aux']
    E         At index 0 diff: 'ps' != 'sudo'
    E         Right contains one more item: 'aux'
    E         Use -v to get the full diff
    
    test_exercise.py:24: AssertionError
    =========================== short test summary info ============================
    FAILED test_exercise.py::TestAdminCommand::test_sudo - AssertionError: assert...
    ========================= 1 failed, 1 passed in 0.04s ==========================
    

    La sortie échoue dans le test test_sudo(). Pytest fournit des détails sur les deux listes en cours de comparaison. Dans le cas présent, la variable result n’a pas la commande sudo dedans, ce que le test attend.

Étape 3 : corriger le bogue pour que les tests réussissent

Avant d’apporter des modifications, vous devez comprendre pourquoi il y a un échec en premier lieu. Même si vous pouvez voir que le but souhaité n’a pas été atteint (sudo n’est pas dans le résultat), vous devez trouver pourquoi.

Examinez les lignes de code suivantes de la fonction admin_command() lorsque la condition sudo=True est remplie :

    if sudo:
        ["sudo"] + command

Actuellement, l’opération des listes n’est pas utilisée pour retourner la valeur. Étant donné qu’elle n’est pas retournée, la fonction finit par retourner la commande toujours sans sudo.

  1. Mettez à jour la fonction admin_command() pour retourner l’opération de liste afin que le résultat modifié soit utilisé lors de la demande d’une commande sudo. La fonction mise à jour doit ressembler à ceci :

    def admin_command(command, sudo=True):
        """
        Prefix a command with `sudo` unless it is explicitly not needed. Expects
        `command` to be a list.
        """
        if sudo:
            return ["sudo"] + command
        return command
    
  2. Réexécutez le test avec Pytest. Essayez d’augmenter le niveau de détail de la sortie en utilisant l’indicateur -v avec Pytest :

    $ pytest -v test_exercise.py
    
  3. Vérifiez ensuite la sortie. Elle doit maintenant afficher deux tests réussis :

    ============================= test session starts ==============================
    Python 3.9.6, pytest-6.2.5, py-1.11.0, pluggy-1.0.0 
    cachedir: .pytest_cache
    rootdir: /private
    collected 2 items
    
    test_exercise.py::TestAdminCommand::test_no_sudo PASSED                  [ 50%]
    test_exercise.py::TestAdminCommand::test_sudo PASSED                     [100%]
    
    ============================== 2 passed in 0.00s ===============================
    

Notes

Étant donné que la fonction peut fonctionner avec davantage de valeurs avec une casse différente, des tests supplémentaires doivent être ajoutés pour couvrir ces variations. Cette opération vise à éviter que des modifications apportées ultérieurement à la fonction provoquent un comportement différent (inattendu).

Étape 4 : ajouter un nouveau code avec des tests

Après avoir ajouté des tests dans les étapes précédentes, vous devriez vous sentir à l'aise pour apporter d'autres modifications à la fonction et les vérifier à l'aide de tests. Même si les modifications ne sont pas couvertes par les tests existants, vous pouvez rester confiant, vous ne cassez aucune hypothèse précédente.

Dans le cas présent, la fonction admin_command() approuve les yeux fermés que l’argument command est toujours une liste. Améliorons cela en veillant à ce qu’une exception avec un message d’erreur utile soit déclenchée.

  1. Tout d’abord, créez un test qui capture le comportement. Bien que la fonction n’a pas encore été mise à jour, essayez une approche « test-first » (également appelée développement piloté par les tests ou TDD).

    • Mettez à jour le fichier test_exercise.py afin qu’il importe pytest en haut. Ce test utilise une assistance interne du framework pytest :
    import pytest
    
    • Ajoutez maintenant un nouveau test à la classe pour vérifier l’exception. Ce test doit s’attendre à une TypeError de la fonction lorsque la valeur qui lui est transmise n’est pas une liste :
        def test_non_list_commands(self):
            with pytest.raises(TypeError):
                admin_command("some command", sudo=True)
    
  2. Réexécutez les tests avec Pytest, ils devraient tous aboutir :

    ============================= test session starts ==============================
    Python 3.9.6, pytest-6.2.5, py-1.11.0, pluggy-1.0.0
    rootdir: /private/
    collected 3 items
    
    test_exercise.py ...                                                     [100%]
    
    ============================== 3 passed in 0.00s ===============================
    

    Le test est suffisant pour vérifier TypeError, mais il serait judicieux d’ajouter le code avec un message d’erreur utile.

  3. Mettez à jour la fonction pour déclencher explicitement une TypeError avec un message d’erreur utile :

    def admin_command(command, sudo=True):
        """
        Prefix a command with `sudo` unless it is explicitly not needed. Expects
        `command` to be a list.
        """
        if not isinstance(command, list):
            raise TypeError(f"was expecting command to be a list, but got a {type(command)}")
        if sudo:
            return ["sudo"] + command
        return command
    
  4. Enfin, mettez à jour la méthode test_non_list_commands() pour rechercher le message d’erreur :

    def test_non_list_commands(self):
        with pytest.raises(TypeError) as error:
            admin_command("some command", sudo=True)
        assert error.value.args[0] == "was expecting command to be a list, but got a <class 'str'>"
    

    Le test mis à jour utilise error comme variable qui contient toutes les informations d’exception. En utilisant error.value.args, vous pouvez examiner les arguments de l’exception. Dans le cas présent, le premier argument a la chaîne d’erreur que le test peut vérifier.

Vérifier votre travail

À ce stade, vous devez avoir un fichier de test Python appelé test_exercise.py qui inclut :

  • Fonction admin_command() qui accepte un argument et un argument de mot clé.
  • Exception TypeError avec un message d’erreur utile dans la fonction admin_command().
  • Classe de test TestAdminCommand() qui a une méthode d’assistance command() et trois méthodes de test qui vérifient la fonction admin_command().

Tous les tests doivent passer sans erreur lorsque vous les exécutez dans le terminal.