Esercizio
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
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'argomentocommand
e, facoltativamente, può aggiungere all'elenco il prefissosudo
. Se l'argomento della parola chiavesudo
è impostato suFalse
, restituisce lo stesso comando specificato come input.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 variabileresult
non contiene il comandosudo
, 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
.
Aggiornare la funzione
admin_command()
per restituire l'operazione di elenco in modo che il risultato modificato venga usato quando si richiede un comandosudo
. 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
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
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.
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 delpytest
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)
- Aggiornare il file test_exercise.py in modo che importi
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.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
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. Conerror.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 funzioneadmin_command()
. - Classe di test
TestAdminCommand()
con un metodo helpercommand()
e tre metodi di test che controllano la funzioneadmin_command()
.
Tutti i test devono essere superati senza errori quando vengono eseguiti nel terminale.