Übung

Abgeschlossen

In dieser Übung verwenden Sie Pytest, um eine Funktion zu testen. Anschließend finden und beheben Sie einige potenzielle Probleme mit der Funktion, die zu fehlerhaften Tests führen. Beim Betrachten von Fehlern und dem Verwenden der umfassenden Fehlerberichterstattung von Pytest ist es wichtig, problematische Tests oder Fehler im Produktionscode zu identifizieren und zu beheben.

Für diese Übung verwenden wir eine Funktion namens admin_command(), die einen Systembefehl als Eingabe entgegennimmt und optional mithilfe des sudo-Tools mit einem Präfix versieht. Die Funktion weist einen Fehler auf, den Sie durch Schreiben von Tests ermitteln.

Schritt 1: Hinzufügen einer Datei mit Tests für diese Übung

  1. Erstellen Sie eine neue Testdatei, indem Sie die Python-Konventionen für Dateinamen für Testdateien befolgen. Nennen Sie die Testdatei test_exercise.py, und fügen Sie den folgenden Code hinzu:

    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
    

    Die Funktion admin_command() verwendet eine Liste als Eingabe mithilfe des command-Arguments und kann optional die Liste mit dem Präfix sudo versehen. Wenn das sudo-Schlüsselwortargument auf False festgelegt ist, gibt es denselben Befehl zurück, der als Eingabe angegeben wird.

  2. Fügen Sie in derselben Datei die Tests für die Funktion admin_command() an. Die Tests verwenden eine Hilfsmethode, die einen Beispielbefehl zurückgibt:

    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
    

Hinweis

Es ist nicht üblich, Tests innerhalb derselben Datei wie tatsächlichen Code aufzubewahren. Die Beispiele in dieser Übung verfügen der Einfachheit halber über tatsächlichen Code in derselben Datei. In realen Python-Projekten werden Tests in der Regel durch Dateien und Verzeichnisse vom Code getrennt, den sie testen.

Schritt 2: Ausführen der Tests und Ermitteln des Fehlers

Da die Testdatei nun eine Funktion zum Testen und eine Reihe von Tests zur Überprüfung ihres Verhaltens enthält, können Sie die Tests nun ausführen und mit Fehlern arbeiten.

  • Führen Sie die Datei mit Python aus:

    $ pytest test_exercise.py
    

    Der Lauf sollte mit einem bestandenen und einem fehlgeschlagenen Test abgeschlossen werden, und die Fehlerausgabe sollte ungefähr so aussehen wie die folgende Ausgabe:

    =================================== 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 ==========================
    

    Fehler in der Ausgabe beim test_sudo()-Test. Pytest gibt Details zu den beiden verglichenen Listen an. In diesem Fall enthält die result-Variable keinen sudo-Befehl. Das ist das erwartete Ergebnis des Tests.

Schritt 3: Beheben des Fehlers und erfolgreiche Durchführung der Tests

Bevor Sie Änderungen vornehmen, müssen Sie verstehen, warum ein Fehler auftritt. Auch wenn Sie sehen können, dass die Erwartung nicht erfüllt wird (sudo ist nicht im Ergebnis enthalten), müssen Sie die Ursache herausfinden.

Sehen Sie sich die folgenden Codezeilen aus der admin_command()-Funktion an, wenn die sudo=True-Bedingung erfüllt ist:

    if sudo:
        ["sudo"] + command

Der Vorgang der Listen wird nicht verwendet, um den Wert zurückzugeben. Da er nicht zurückgegeben wird, gibt die Funktion den Befehl letztlich stets ohne sudo zurück.

  1. Aktualisieren Sie die admin_command()-Funktion, damit der Listenvorgang zurückgegeben wird, sodass das geänderte Ergebnis beim Anfordern eines sudo-Befehls verwendet wird. Die aktualisierte Funktion sollte wie folgt aussehen:

    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. Führen Sie den Test mit Pytest erneut aus. Versuchen Sie, die Ausführlichkeit der Ausgabe zu verbessern, indem Sie das -v-Flag mit Pytest verwenden:

    $ pytest -v test_exercise.py
    
  3. Überprüfen Sie nun die Ausgabe. Nun sollten zwei bestandene Tests gezeigt werden:

    ============================= 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 ===============================
    

Hinweis

Da die Funktion mit mehreren Werten verschiedener Schreibung arbeiten kann, sollten weitere Tests hinzugefügt werden, um diese Variationen abzudecken. Dadurch kann in Zukunft verhindert werden, dass Änderungen an der Funktion ein anderes (unerwartetes) Verhalten hervorrufen.

Schritt 4: Hinzufügen von neuem Code mit Tests

Nach dem Hinzufügen von Tests in den vorherigen Schritten sollten Sie sich wohl fühlen, weitere Änderungen an der Funktion vorzunehmen und sie mit Tests zu überprüfen. Selbst wenn die Änderungen nicht durch vorhandene Tests abgedeckt sind, können Sie sicher sein, dass Sie gegen keine vorherigen Annahmen verstoßen.

In diesem Fall vertraut die admin_command()-Funktion blind darauf, dass das command-Argument stets eine Liste ist. Wir können dies verbessern, indem wir sicherstellen, dass eine Ausnahme mit einer hilfreichen Fehlermeldung ausgelöst wird.

  1. Erstellen Sie zunächst einen Test, der das Verhalten erfasst. Obwohl die Funktion noch nicht aktualisiert wurde, versuchen Sie einen Test-First-Ansatz (auch als testgesteuerte Entwicklung oder TDD bezeichnet).

    • Aktualisieren Sie die Datei test_exercise.py, damit sie pytest oben importiert. Dieser Test verwendet ein internes Hilfsprogramm aus dem pytest-Framework:
    import pytest
    
    • Fügen Sie nun einen neuen Test an die Klasse an, um die Ausnahme zu überprüfen. Dieser Test sollte eine TypeError von der Funktion erwarten, wenn der an sie übergebene Wert keine Liste ist:
        def test_non_list_commands(self):
            with pytest.raises(TypeError):
                admin_command("some command", sudo=True)
    
  2. Führen Sie die Tests erneut mit Pytest aus. Sie sollten sie alle bestehen:

    ============================= 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 ===============================
    

    Der Test ist gut genug für eine Überprüfung von TypeError. Es wäre allerdings gut, den Code mit einer hilfreichen Fehlermeldung hinzuzufügen.

  3. Aktualisieren Sie die Funktion, um TypeError explizit mit einer hilfreichen Fehlermeldung auszulösen:

    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. Aktualisieren Sie schließlich die test_non_list_commands()-Methode, um nach der Fehlermeldung zu suchen:

    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'>"
    

    Der aktualisierte Test verwendet error als Variable, die alle Ausnahmeinformationen enthält. Mithilfe von error.value.args können Sie die Argumente der Ausnahme untersuchen. In diesem Fall weist das erste Argument die Fehlerzeichenfolge auf, die der Test überprüfen kann.

Arbeit überprüfen

Jetzt sollten Sie eine Python-Testdatei namens test_exercise.py haben, die Folgendes enthält:

  • Eine admin_command()-Funktion, die ein Argument akzeptiert, und ein Schlüsselwortargument
  • Eine TypeError-Ausnahme mit einer hilfreichen Fehlermeldung in der admin_command()-Funktion
  • Eine TestAdminCommand()-Testklasse mit einer command()-Hilfsmethode und drei Testmethoden, die die admin_command()-Funktion überprüfen

Alle Tests sollten ohne Fehler übergeben werden, wenn Sie sie im Terminal ausführen.