Esercizio

Completato

In questo esercizio si usa Pytest per testare una funzione. Si individuano quindi e si risoducono alcuni potenziali problemi con la funzione che causano test non superati. Esaminare gli errori e usare la segnalazione errori avanzata di Pytest è essenziale per identificare e correggere i test problematici o i bug nel codice di produzione.

Per questo esercizio viene usata una funzione denominata admin_command() che accetta un comando di sistema come input e, facoltativamente, lo antepone allo sudo strumento. La funzione presenta un bug, che viene rilevato scrivendo test.

Passaggio 1 - Aggiungere un file con test per questo esercizio

  1. Usare le convenzioni del nome file di Python per i file di test per creare un nuovo file di test. Assegnare al file di test il nome test_exercise.py e aggiungere il codice seguente:

    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 funzione admin_command() accetta un elenco come input usando l'argomento command e, facoltativamente, può aggiungere all'elenco il prefisso sudo. Se l'argomento della parola chiave sudo è impostato su False, restituisce lo stesso comando specificato come input.

  2. Nello stesso file aggiungere i test per la funzione admin_command(). I test usano un metodo helper che restituisce un comando di esempio:

    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
    

Nota

La presenza di test come codice effettivo nello stesso file non è comune. Per semplicità, tuttavia, negli esempi di questo esercizio sarà presente codice effettivo nello stesso file. Nei progetti Python reali, i test sono in genere separati da file e directory dal codice di cui stanno eseguendo il test.

Passaggio 2 - Eseguire i test e identificare l'errore

Ora che per il file di test è presente una funzione da testare e un paio di test per verificarne il comportamento, è possibile eseguire i test e usare gli errori.

  • Eseguire il file con Python:

    $ pytest test_exercise.py
    

    L'esecuzione deve essere completata con un test superato e un errore e l'output dell'errore deve essere simile all'output seguente:

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

    L'output genera un errore nel test test_sudo(). Pytest fornisce informazioni dettagliate sui due elenchi confrontati. In questo caso, la variabile result non contiene il comando sudo, ovvero quello previsto dal test.

Passaggio 3 - Correggere il bug e fare in modo che i test vengano superati

Prima di apportare modifiche, è necessario comprendere il motivo per cui si è verificato un errore. Anche se si può vedere che l'aspettativa non viene soddisfatta (sudo non è nel risultato), è necessario scoprire perché.

Esaminare le righe di codice seguenti con la funzione admin_command()quando viene soddisfatta la condizione sudo=True:

    if sudo:
        ["sudo"] + command

L'operazione degli elenchi non viene usata per restituire il valore. Poiché il valore non viene restituito, la funzione termina con la restituzione del comando sempre senza sudo.

  1. Aggiornare la funzione admin_command() per restituire l'operazione di elenco in modo che il risultato modificato venga usato quando si richiede un comando sudo. La funzione completata ha l'aspetto seguente:

    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. Eseguire di nuovo il test con Pytest. Provare ad aumentare il livello di dettaglio dell'output usando il flag -v con Pytest:

    $ pytest -v test_exercise.py
    
  3. A questo punto verificare l'output. Ora dovrebbero essere visualizzati due test superati:

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

Nota

Poiché la funzione è in grado di usare più valori con maiuscole e minuscole diverse, è necessario aggiungere più test per considerare tali varianti. Ciò consente di evitare che modifiche future alla funzione provochino un comportamento diverso (imprevisto).

Passaggio 4 - Aggiungere nuovo codice con i test

Dopo aver aggiunto i test nei passaggi precedenti, è consigliabile apportare altre modifiche alla funzione e verificarle con i test. Anche se le modifiche non sono coperte da test esistenti, si ha la certezza di non violare alcuna ipotesi precedente.

In questo caso, la funzione admin_command() è certa che l'argomento command è sempre un elenco. È possibile migliorare questa situazione assicurandosi che venga generata un'eccezione con un messaggio di errore utile.

  1. Creare innanzitutto un test che acquisisce il comportamento. Anche se la funzione non è ancora aggiornata, provare un approccio test-first (noto anche come Sviluppo basato su test o TDD).

    • Aggiornare il file test_exercise.py in modo che importi pytest nella parte superiore. Questo test usa un helper interno del pytest framework:
    import pytest
    
    • Aggiungere ora un nuovo test alla classe per verificare l'eccezione. Questo test dovrebbe aspettarsi una TypeError classe dalla funzione quando il valore passato non è un elenco:
        def test_non_list_commands(self):
            with pytest.raises(TypeError):
                admin_command("some command", sudo=True)
    
  2. Usare Pytest per eseguire di nuovo i test, che dovrebbero essere tutti superati:

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

    Il test è sufficiente per verificare TypeError, ma è consigliabile aggiungere il codice con un messaggio di errore utile.

  3. Aggiornare la funzione per generare un'eccezione TypeError in modo esplicito con un messaggio di errore 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. Aggiornare infine il metodo test_non_list_commands() per verificare la presenza del messaggio di errore:

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

    Il test aggiornato usa error come variabile che contiene tutte le informazioni sull'eccezione. Con error.value.args è possibile esaminare gli argomenti dell'eccezione. In questo caso, il primo argomento contiene la stringa di errore che il test può controllare.

Controlla il tuo lavoro

A questo punto è necessario avere un file di test Python denominato test_exercise.py che include:

  • Funzione admin_command() che accetta un argomento e un argomento di parola chiave.
  • Eccezione TypeError con un messaggio di errore utile nella funzione admin_command().
  • Classe di test TestAdminCommand() con un metodo helper command() e tre metodi di test che controllano la funzione admin_command().

Tutti i test devono essere superati senza errori quando vengono eseguiti nel terminale.