Articolo originale: How to Write Unit Tests for Python Functions

Questa guida ti insegnerà come scrivere unit test per funzioni Python. Ma perché dovresti prendere in considerazione la scrittura di unit test?

Quando si lavora su un grande progetto, spesso dovrai aggiornare certi moduli e svolgere operazioni di refactoring sul codice secondo necessità. Tuttavia queste modifiche potrebbero avere conseguenze indesiderate su altri moduli che usano al loro interno il modulo aggiornato. Questo talvolta può rompere delle funzionalità esistenti.

Come sviluppatore, dovresti testare il tuo codice per assicurarti che tutti i moduli in un'applicazione funzionino a dovere. Gli unit test ti consentono di verificare se piccole, isolate, unità di codice funzionano correttamente e fanno sì che tu possa correggere incongruenze che potrebbero sorgere dopo aggiornamenti e operazioni di refactoring.

Questa guida ti aiuterà a iniziare con lo unit testing in Python. Imparerai come usare il modulo Python integrato unittest per impostare ed eseguire gli unit test e scrivere casi di test per funzioni Python. Imparerai anche come testare funzioni che danno eccezioni.

Iniziamo!

Test in Python – Primi Passi

Inizieremo definendo una funzione Python e scriveremo degli unit test per verificare se funziona come previsto. Per focalizzarci su come impostare gli unit test, utilizzeremo una semplice funzione, is_prime() , che riceve un numero e verifica se è un numero primo oppure no.

import math

def is_prime(num):
    '''Verifica se num è un numero primo o no.'''
    for i in range(2, int(math.sqrt(num))+1):
        if num % i == 0:
            return False
    return True

Facciamo partire il REPL di Python, chiamiamo la funzione is_prime() con alcuni argomenti e verifichiamo i risultati.

>>> from prime_number import is_prime
>>> is_prime(3)
True
>>> is_prime(5)
True
>>> is_prime(12)
False
>>> is_prime(8)
False
>>> assert is_prime(7) == True

Puoi anche usare l'istruzione assert per verificare che is_prime() restituisca il valore booleano previsto, come mostrato sopra. Se il valore di ritorno della funzione è diverso da quello previsto, viene sollevata un'eccezione AssertionError.

Questo tipo di test manuali non è efficiente qualora tu voglia verificare in modo esaustivo la tua funzione con un elenco di argomenti molto più grande. Potresti voler impostare test automatici che eseguono e validano il risultato della funzione contro casi di test definiti nella suite di test.

Come Usare il Modulo Python unittest

Python è dotato del modulo unittest che ti consente di configurare test automatizzati per le funzioni e le classi nella tua applicazione. La procedura generica per impostare unit test in Python è la seguente:

# <nome-modulo>.py

import unittest
from <modulo> import <funzione_da_testare>
# tutte le voci tra <> sono segnaposti

class TestClass(unittest.TestCase):
	def test_<nome_1>(self):
		# verifica funzione_da_testare

	def test_<nome_2>(self):
		# verifica funzione_da_testare
	:
	:
	:

	def test_<nome_n>(self):
		# verifica funzione_da_testare

Il codice qui sopra in <nome-modulo>.py fa quanto segue:

  • Importa il modulo integrato di Python unittest.
  • Importa la funzione Python da testare dal modulo nel quale è definita, <modulo>.
  • Crea una classe di test (TestClass) che eredita dalla classe unittest.TestCase.
  • Ciascun test che dovrebbe essere eseguito andrà definito come metodo all'interno della classe di test.
  • 💡 Nota: Affinché il modulo unittest possa identificare questi metodi come test ed eseguirli, i nomi di questi metodi dovrebbero iniziare con  test_.
  • La classe TestCase del modulo unittest fornisce utili metodi di asserzione per verificare se la funzione oggetto di test ritorni i valori previsti.

I più comuni metodi di asserzione sono elencati qui sotto, ne useremo alcuni in questo tutorial.

METODO DESCRIZIONE
assertEqual(valore_atteso, valore_effettivo) Asserisce che valore_atteso == valore_effettivo
assertTrue(risultato) Asserisce che bool(risultato) è True
assertFalse(risultato) Asserisce che bool(risultato) è False
assertRaises(eccezione, funzione, *args, **kwargs) Asserisce che funzione(*args, **kwargs)
solleva la specifica eccezione

📑 Per un elenco completo dei metodi di asserzione, fai riferimento alla documentazione di unittest.

Per eseguire questi test, dovresti eseguire unittest come modulo principale, usando il seguente comando:

$ python -m unittest <nome-modulo>.py

Possiamo aggiungere il costrutto condizionale if __name__=='__main__' per eseguire  unittest come modulo 'main'.

if __name__=='__main__':
	unittest.main()

Aggiungere il codice condizionale qui sopra, ti consente di eseguire i test direttamente eseguendo il modulo che contiene i test.

$ python <nome-modulo>.py

Come Definire Casi di Test per Funzioni Python

unittesting-101


In questa sezione, scriveremo unit test per la funzione is_prime() usando la sintassi che abbiamo appreso.

Per verificare che la funzione is_prime() restituisca un booleano possiamo usare i metodi assertTrue() e assertFalse(). Definiamo quattro metodi di test all'interno della classe TestPrime che eredita da unittest.TestCase.

import unittest

# importa la funzione is_prime
from prime_number import is_prime


class TestPrime(unittest.TestCase):

	def test_two(self):
        self.assertTrue(is_prime(2))
    
    def test_five(self):
    	self.assertTrue(is_prime(5))
    
    def test_nine(self):
    	self.assertFalse(is_prime(9))
    
    def test_eleven(self):
    	self.assertTrue(is_prime(11))
        
        
if __name__=='__main__':
	unittest.main()
$ python test_prime.py

Nel risultato seguente, '.' indica un test che ha avuto successo.

Output
....
----------------------------------------------------------------------
Ran 4 tests in 0.001s
OK

Nel codice qui sopra ci sono quattro metodi di test, ognuno dei quali verifica un determinato input. Puoi invece definire un singolo metodo di test che asserisce se il risultato sia corretto, per tutti e quattro gli input.

import unittest

from prime_number import is_prime


class TestPrime(unittest.TestCase):

	def test_prime_not_prime(self):
        self.assertTrue(is_prime(2))
        self.assertTrue(is_prime(5))
        self.assertFalse(is_prime(9))
        self.assertTrue(is_prime(11))

Dopo l'esecuzione del modulo test_prime, vediamo che è stato eseguito con successo un test. Se uno qualunque dei metodi di asserzione avesse sollevato un'eccezione, allora il test sarebbe fallito.

$ python test_prime.py
Output
.
----------------------------------------------------------------------
Ran 1 test in 0.001s
OK

Come Scrivere Unit Test per Verificare Eccezioni

Nella sezione precedente, abbiamo testato la funzione is_prime() con numeri primi e non primi come input. Nello specifico erano tutti numeri positivi interi.

Non abbiamo ancora imposto che gli argomenti per la chiamata della funzione is_prime() siano interi positivi. Puoi usare il type hinting per imporre tipi o sollevare eccezioni per input non validi.

Nel test della funzione is_prime(), non abbiamo tenuto conto di quanto segue:

  • Se l'argomento è un valore a virgola mobile, is_prime() sarebbe comunque eseguita e restituirebbe True o False, il che non è corretto.
  • Per argomenti di altro tipo, ad esempio la stringa 'cinque' invece che il numero 5, la funzione dà un errore TypeError.
  • Se l'argomento è un intero negativo, allora la funzione math.sqrt() dà un ValueError. Le radici di tutti i numeri reali (positivi, negativi o zero) sono sempre non negative. Quindi la radice quadrata è definita solo per numeri non negativi.

Verifichiamo quanto sopra affermato eseguendo qualche esempio nel REPL di Python.

>>> from prime_number import is_prime

>>> is_prime('five')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/home/bala/unit-test-1/prime_number.py", line 5, in is_prime
for i in range(2,int(math.sqrt(num))+1):
TypeError: must be real number, not str

>>> is_prime(-10)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/home/bala/unit-test-1/prime_number.py", line 5, in is_prime
for i in range(2,int(math.sqrt(num))+1):
ValueError: math domain error

>>> is_prime(2.5)
True

Come Sollevare Eccezioni per Input Non Validi

Per risolvere quanto sopra, validiamo il valore usato nella chiamata della funzione per num e solleviamo un'eccezione quando necessario.

  • Verifichiamo se num sia un intero. In caso positivo, si procede alla prossima verifica, altrimenti viene sollevata un'eccezione TypeError.
  • Verifichiamo se num sia un numero negativo. Se lo è viene sollevata un'eccezione ValueError.

Dopo la modifica della definizione della funzione per validare l'input e sollevare eccezioni il codice sarà:

import math

def is_prime(num):
    '''Verifica se num sia un numero primo oppure no.'''
    # solleva TypeError per un tipo in input non valido
    if type(num) != int:
        raise TypeError('il tipo di num non è non valido')
    # solleva ValueError per un valore negativo
    if num < 0:
        raise ValueError('Verifica il valore di num; num è un valore non negativo?')
    # se l'input è valido si procede per verificare se num sia un numero primo
    for i in range(2, int(math.sqrt(num))+1):
        if num % i == 0:
        	return False
    return True

Ora che abbiamo modificato la funzione affinché sollevi eccezioni ValueError e TypeError per input non validi, il prossimo passo è di verificare se queste eccezioni vengono sollevate.

Come Usare il Metodo assertRaises() per Testare Eccezioni

test-exceptions

Nella definizione della classe TestPrime, aggiungiamo metodi per verificare se le eccezioni vengono sollevate.

Definiamo i metodi test_typeerror_1() e test_typeerror_2() per verificare se l'eccezione TypeError viene sollevata, e il metodo test_valueerror() per verificare se viene sollevata l'eccezione ValueError.

📌 Per chiamare il metodo assertRaises() , possiamo usare la seguente sintassi generale:

def test_exception(self):
    self.assertRaises(nome-eccezione,nome-funzione,args)

Possiamo anche usare il gestore di contesto con la seguente sintassi (che adotteremo nell'esempio successivo):

def test_exception(self):
    with self.assertRaises(nome-eccezione):
        nome-funzione(args)

Aggiungendo i metodi di test per la verifica delle eccezioni, il codice sarà:

import unittest

from prime_number import is_prime


class TestPrime(unittest.TestCase):

	def test_prime_not_prime(self):
        self.assertTrue(is_prime(2))
        self.assertTrue(is_prime(5))
        self.assertFalse(is_prime(9))
        self.assertTrue(is_prime(11))
    
    def test_typeerror_1(self):
        with self.assertRaises(TypeError):
        	is_prime(6.5)
    
    def test_typeerror_2(self):
        with self.assertRaises(TypeError):
        	is_prime('five')
    
    def test_valueerror(self):
        with self.assertRaises(ValueError):
        	is_prime(-4)
            
if __name__=='__main__':
	unittest.main()

Eseguiamo il modulo test_prime e osserviamo il risultato:

$ python test_prime.py
Output
....
----------------------------------------------------------------------
Ran 4 tests in 0.002s
OK

Nel codice degli esempi che abbiamo scritto fino a ora tutti i test hanno avuto successo. Modifichiamo uno dei metodi, diciamo test_typeerror_2(), in questo modo:

def test_typeerror_2(self):
    with self.assertRaises(TypeError):
    	is_prime(5)

Chiamiamo la funzione is_prime() con il numero 5 come argomento. In questo caso, 5 è un input valido per il quale la funzione ritorna True. Pertanto la funzione non dà TypeError. Quando eseguiamo il test nuovamente, vedremo che ci sarà un test che fallisce.

$ python test_prime.py
Output

..F.
======================================================================
FAIL: test_typeerror_2 (__main__.TestPrime)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_prime.py", line 17, in test_typeerror_2
is_prime(5)
AssertionError: TypeError not raised
----------------------------------------------------------------------
Ran 4 tests in 0.003s
FAILED (failures=1)

Conclusione

Grazie per avere letto fino a qui! 😄 Spero che questo tutorial ti abbia aiutato a comprendere le basi per eseguire unit test in Python.

Hai imparato a impostare i test e verificare se una funzione si comporta come previsto oppure solleva un'eccezione — tutto questo usando il modulo integrato di Python unittest.

Continua a programmare, e ci vediamo al prossimo tutorial!👩🏽‍💻