Esercizio
In questo esercizio si testa una funzione usando Pytest. Successivamente vengono individuati e risolti alcuni potenziali problemi che causano test con esito negativo. L'analisi degli errori e l'uso della segnalazione errori dettagliata di Pytest sono elementi essenziali per identificare e correggere i test problematici oppure i bug nel codice di produzione.
Questo esercizio prevede l'uso di una funzione denominata admin_command() che accetta un comando di sistema come input e, facoltativamente, gli assegna un prefisso con lo strumento sudo. La funzione presenta un bug 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 commandLa funzione
admin_command()accetta un elenco come input usando l'argomentocommande, 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à, gli esempi in questo esercizio hanno 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.pyL'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 variabileresultnon 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ò notare che l'aspettativa non viene soddisfatta (sudo non è presente nel risultato), è necessario scoprire il motivo.
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 commandEseguire di nuovo il test con Pytest. Provare ad aumentare il livello di dettaglio dell'output usando il flag
-vcon Pytest:$ pytest -v test_exercise.pyA 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, si dovrebbe poter apportare agevolmente altre modifiche alla funzione e verificarle con 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 funzionalità non è ancora aggiornata, provare a utilizzare un approccio test-first (noto anche come Sviluppo guidato dai test o TDD).
- Aggiornare il file test_exercise.py in modo che importi
pytestnella parte superiore. Questo test usa un helper interno del frameworkpytest:
import pytest- Aggiungere ora un nuovo test alla classe per verificare l'eccezione. Questo test dovrebbe aspettarsi un
TypeErrordalla 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
Eseguire di nuovo i test con Pytest. Devono passare tutti:
============================= 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
TypeErrorin 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 commandAggiornare 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
errorcome 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.
Eseguire il test di quanto fatto finora
A questo punto si dovrebbe 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
TypeErrorcon 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 superare senza errori quando vengono eseguiti nel terminale.