Övning

Slutförd

I den här övningen använder du Pytest för att testa en funktion. Sedan hittar och åtgärdar du några potentiella problem med funktionen som orsakar misslyckade tester. Att titta på fel och använda Pytests omfattande felrapportering är viktigt för att identifiera och åtgärda problematiska tester eller buggar i produktionskoden.

I den här övningen admin_command() använder vi en funktion med namnet sudo som accepterar ett systemkommando som indata och som eventuellt prefixar det med verktyget. Funktionen har en bugg som du upptäcker genom att skriva tester.

Steg 1 – Lägga till en fil med tester för den här övningen

  1. Skapa en ny testfil med hjälp av Pythons filnamnskonventioner för testfiler. Ge testfilen namnet test_exercise.py och lägg till följande kod:

    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
    

    Funktionen admin_command() tar en lista som indata med argumentet command och kan eventuellt prefixa listan med sudo. Om nyckelordsargumentet sudo är inställt på Falsereturnerar det samma kommando som anges som indata.

  2. I samma fil lägger du till testerna för admin_command() funktionen. Testerna använder en hjälpmetod som returnerar ett exempelkommando:

    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
    

Kommentar

Det är inte vanligt att ha tester i samma fil som den faktiska koden. För enkelhetens skull har exemplen i den här övningen faktisk kod i samma fil. I verkliga Python-projekt separeras tester vanligtvis av filer och kataloger från koden som de testar.

Steg 2 – Kör testerna och identifiera felet

Nu när testfilen har en funktion att testa och ett par tester för att verifiera dess beteende är det dags att köra testerna och arbeta med fel.

  • Kör filen med Python:

    $ pytest test_exercise.py
    

    Körningen ska slutföras med ett test som passerar och ett fel, och felutdata bör likna följande utdata:

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

    Utdata misslyckas i test_sudo() testet. Pytest ger information om de två listor som jämförs. I det här fallet har result variabeln sudo inte kommandot i sig, vilket är vad testet förväntar sig.

Steg 3 – Åtgärda felet och få testerna att passera

Innan du gör några ändringar måste du först förstå varför det uppstår ett fel. Även om du kan se att förväntningarna inte uppfylls (sudo inte är i resultatet), måste du ta reda på varför.

Titta på följande kodrader från admin_command() funktionen när villkoret sudo=True uppfylls:

    if sudo:
        ["sudo"] + command

Listornas åtgärd används inte för att returnera värdet. Eftersom den inte returneras returnerar funktionen kommandot utan sudo alltid.

  1. admin_command() Uppdatera funktionen för att returnera liståtgärden så att det ändrade resultatet används när du begär ett sudo kommando. Den uppdaterade funktionen bör se ut så här:

    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. Kör testet igen med Pytest. Prova att öka utförligheten för utdata med hjälp -v av flaggan med Pytest:

    $ pytest -v test_exercise.py
    
  3. Kontrollera nu utdata. Den bör nu visa två godkänt test:

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

Kommentar

Eftersom funktionen kan fungera med fler värden för olika höljen bör fler tester läggas till för att täcka dessa variationer. Detta skulle förhindra att framtida ändringar i funktionen orsakar ett annat (oväntat) beteende.

Steg 4 – Lägga till ny kod med tester

När du har lagt till tester i föregående steg bör du känna dig bekväm med att göra fler ändringar i funktionen och verifiera dem med tester. Även om ändringarna inte omfattas av befintliga tester kan du känna dig säker på att du inte bryter mot tidigare antaganden.

I det här fallet admin_command() litar funktionen blint på att command argumentet alltid är en lista. Nu ska vi förbättra det genom att se till att ett undantag med ett användbart felmeddelande genereras.

  1. Skapa först ett test som fångar upp beteendet. Även om funktionen inte har uppdaterats ännu kan du prova en test-first-metod (kallas även testdriven utveckling eller TDD).

    • Uppdatera test_exercise.py-filen så att den importeras pytest längst upp. Det här testet använder en intern hjälp från ramverket pytest :
    import pytest
    
    • Lägg nu till ett nytt test i klassen för att kontrollera undantaget. Det här testet bör förvänta sig en TypeError från funktionen när värdet som skickas till den inte är en lista:
        def test_non_list_commands(self):
            with pytest.raises(TypeError):
                admin_command("some command", sudo=True)
    
  2. Kör testerna igen med Pytest, de bör alla godkännas:

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

    Testet är tillräckligt bra för TypeError att söka efter, men det skulle vara bra att lägga till koden med ett användbart felmeddelande.

  3. Uppdatera funktionen för att skapa en TypeError explicit med ett användbart felmeddelande:

    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. Uppdatera slutligen test_non_list_commands() metoden för att söka efter felmeddelandet:

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

    Det uppdaterade testet använder error som en variabel som innehåller all undantagsinformation. Med kan error.value.argsdu titta på argumenten för undantaget. I det här fallet har det första argumentet den felsträng som testet kan kontrollera.

Kontrollera ditt arbete

Nu bör du ha en Python-testfil med namnet test_exercise.py som innehåller:

  • En admin_command() funktion som accepterar ett argument och ett nyckelordsargument.
  • Ett TypeError undantag med ett användbart felmeddelande i admin_command() funktionen.
  • En TestAdminCommand() testklass som har en command() hjälpmetod och tre testmetoder som kontrollerar admin_command() funktionen.

Alla tester bör skickas utan fel när du kör dem i terminalen.