Testarea unitara
Testarea unitara este un proces care permite evaluarea automata a codului de la nivelul unei aplicatii. Reprezinta un aspect foarte important, mai ales daca avem in vedere modificarile/actualizarile care pot fi aduse unei aplicatii.
Caracteristicile de baza ale procesului de testare unitara includ urmatoarela aspecte:
• fiecare test valideaza o functionalitatea de la nivelul aplicatiei;
• testele trebuie sa ruleze rapid, sa fie scurte si usor de inteles;
• sa nu fie necesare configurari suplimentare.
Printre avantajele testarii unitare se numara cresterea vitezei de scriere a codului, cresterea performantelor codului, identificarea mult mai rapida a erorilor si reducerea timpului necesar depanarii/intretinerii unei aplicatii.
Modul | Descriere |
---|---|
unittest | modul pentru testare unitara care insoteste toate distributiile Python |
nose | extensie a modulului unittest care permite o implementare mai facila a testarii unitare |
doctest | modul standard care permite testarea prin intermediul unor sesiuni Python interactive introduse la nivelul sirurilor de documentare |
Framework-ul PyUnit
PyUnit este un framework pentru testare unitara, similar cu JUnit (testare unitara pentru limbajul Java). Permite testarea automata, distributia codului de initializare sau eliberare a resurselor pentru teste si agregarea testelor in colectii.
Testarea unitara realizata prin intermediul framework-ului PyUnit (modulul unittest) are in vedere urmatoarele concepte: test fixture, test case, test suite si test runner. Pregatirile necesare pentru realizarea testelor, dar si actiunile de eliberare a resurselor sunt grupate sub numele de test fixture.
Un test sau test case reprezinta o unitate de testare care verifica raspunsul pentru un set de valori de intrare specificate. Modulul unittest ofera o clasa de baza, numita TestCase, clasa care poate fi utilizata pentru a implementa teste. Agregarea testelor care trebuie sa fie rulate impreuna se realizeaza prin intermediul unei colectii de teste unitare cunoscuta sub numele de test suite.
Componenta care orchestreaza executia testelor si ofera rezultatul utilizatorului porta numele de test runner. Poate rula in interfata grafica sau in mod text, si poate returna o valoare pentru a indica rezultatul executiei testelor.
"""Rolul acestui modul este acela de a introduce functii pentru transmiterea de mesaje. | |
Fisier: hello.py | |
Autor: airman | |
Versiune Python: 3.7""" | |
default_name = 'Ghita' | |
def hello_python(): | |
"""Returneaza un mesaj de tipul Hello Python!""" | |
return 'Hello Python!' | |
def hello_name(name = default_name): | |
"""Returneaza un mesaj personalizat sau Hello Ghita! daca nu se transmite un nume.""" | |
if len(name) <= 2: | |
raise ValueError("Invalid name!") | |
return 'Hello {}!'.format(name) |
Modulul unittest ofera o colectie importanta de unelte ce permit constructia si rularea de teste. O unitate de testare/evaluare este creata prin derivarea clasei TestCase din modulul unittest.
Metode individuale pentru evaluarea functiilor de la nivelul modulului hello sunt introduse la nivelul clasei de test TestHello. Prin conventie, denumirea metodelor de test incepe cu termenul test_. In acest fel este instiintata si componenta care orchestreaza executia testelor si ofera rezultatul utilizatorului (test runner) cu privire la metodele de test.
Punctul central al fiecarui test este un apel al unor metode, precum: assertEqual() pentru a verifica un rezultat asteptat, assertTrue() sau assertFalse() pentru a verifica o conditie, sau assertRaises() pentru a verifica daca este generata o anumita exceptie. Astfel de metode sunt utilizate pentru ca test runner-ul sa poata obtine rezultatele rularii testelor si sa genereze un raport.
import unittest | |
import hello | |
class TestHello(unittest.TestCase): | |
def test_hello_python(self): | |
self.assertEqual(hello.hello_python(), "Hello Python!") | |
def test_hello_name(self): | |
self.assertEqual(hello.hello_name(), "Hello Ghita!") | |
self.assertEqual(hello.hello_name("Marian"), "Hello Marian!") | |
self.assertRaises(ValueError, hello.hello_name, "M") | |
with self.assertRaises(ValueError): | |
hello.hello_name("M") | |
if __name__ == "__main__": | |
unittest.main() |
Clasa de test poate contine metode de tip setUp() si/sau tearDown() care se executa inainte si dupa fiecare metoda de tip test pentru a initializa/configura, respectiv elibera resursele utilizate in procesul de testare.
C:\Users\airman\virtualcampus\py>python test_hello.py .. ---------------------------------------------------------------------- Ran 2 tests in 0.000s OK
O modalitatea simpla de rularea a testelor o reprezinta unittest.main(), secventa care asigura o interfata in linia de comanda corespunzatoare rularii testelor.
C:\Users\airman\virtualcampus\py>python -m unittest test_hello.py .. ---------------------------------------------------------------------- Ran 2 tests in 0.000s OK
C:\Users\airman\virtualcampus\py>python -m unittest test_hello.TestHello .. ---------------------------------------------------------------------- Ran 2 tests in 0.000s OK
C:\Users\airman\virtualcampus\py>python -m unittest test_hello.TestHello.test_hello_name . ---------------------------------------------------------------------- Ran 1 test in 0.000s OK
Organizarea testelor
Unitatile de baza din testarea unitara poarta numele de test cases si reprezinta scenarii individuale care trebuie initializate si evaluate. O astfel de unitate de testare este o instanta a clasei unittest.TestCase.
class Facultate(): | |
'''Clasa Facultate''' | |
finantare_per_student = 230 | |
def __init__(self, nume, acronim, studenti): | |
self.nume = nume | |
self.acronim = acronim | |
self.studenti = studenti | |
@property | |
def nume_facultate(self): | |
return 'Facultatea de {}'.format(self.nume) | |
@property | |
def numar_studenti(self): | |
return 'Numar studenti {} la facultatea {}'.format(self.studenti, self.acronim) | |
@property | |
def finantare(self): | |
return self.finantare_per_student * self.studenti | |
if __name__ == "__main__": | |
f1 = Facultate('Tehnologii Informationale', 'ti', 3720) | |
print(f1.nume_facultate) | |
print(f1.finantare) |
Codul de testare al unei instante TestCase trebuie să fie complet autonom, astfel incat sa poata fi executat fie in mod izolat, fie in combinatie arbitrara cu orice numar de alte unitati de testare. Cea mai simpla subclasa TestCase va implementa pur si simplu o metoda de testare (a carei denumire incepe cu test) pentru a executa un cod de testare specific.
import unittest | |
from facultate import Facultate | |
class TestFacultate(unittest.TestCase): | |
'''Test Facultate''' | |
def test_nume_facultate(self): | |
f1 = Facultate('Tehnologii Informationale', 'ti', 3720) | |
self.assertEqual(f1.nume_facultate, 'Facultatea de Tehnologii Informationale') | |
f1.nume = 'Electronica si Telecomunicatii' | |
self.assertEqual(f1.nume_facultate, 'Facultatea de Electronica si Telecomunicatii') | |
def test_numar_studenti(self): | |
f1 = Facultate('Tehnologii Informationale', 'ti', 3720) | |
self.assertEqual(f1.numar_studenti, 'Numar studenti 3720 la facultatea ti') | |
f1.studenti = 4050 | |
self.assertEqual(f1.numar_studenti, 'Numar studenti 4050 la facultatea ti') | |
def test_finantare(self): | |
f1 = Facultate('Tehnologii Informationale', 'ti', 3720) | |
self.assertEqual(f1.finantare, 855600) | |
if __name__ == "__main__": | |
unittest.main() |
Cum testele pot fi numeroase, iar configurarea lor poate fi repetitiva, exista posibilitatea izolarii codului de initializare intr-o metoda specifica, numita setUp(), pe care cadrul de testare o va solicita automat pentru fiecare test pe care il executam.
In mod similar, putem implementa o metoda tearDown() care se executa dupa executia fiecarei metode de test. Un astfel de mediu de lucru stabilit la nivelul codului de test poarta numele de fixture.
import unittest | |
from facultate import Facultate | |
class TestFacultate(unittest.TestCase): | |
'''Test Facultate''' | |
def setUp(self): | |
self.f1 = Facultate('Tehnologii Informationale', 'ti', 3720) | |
def tearDown(self): | |
del self.f1 | |
def test_nume_facultate(self): | |
self.f1.nume = 'Electronica si Telecomunicatii' | |
self.assertEqual(self.f1.nume_facultate, 'Facultatea de Electronica si Telecomunicatii') | |
def test_numar_studenti(self): | |
self.assertEqual(self.f1.numar_studenti, 'Numar studenti 3720 la facultatea ti') | |
self.f1.studenti = 4050 | |
self.assertEqual(self.f1.numar_studenti, 'Numar studenti 4050 la facultatea ti') | |
def test_finantare(self): | |
self.assertEqual(self.f1.finantare, 855600) | |
if __name__ == "__main__": | |
unittest.main() |
Executia metodelor de test se face in ordinea alfabetica a numelor acestora. Mai multe instante de tip TestCase pot fi grupate in functie de caracteristicile testate. Modulul unittest ofera un mecanism pentru gruparea mai multor elemente de tip TestCase prin intermediul clasei TestSuite.
import unittest | |
from test_facultate import TestFacultate | |
def suite(): | |
suite = unittest.TestSuite() | |
suite.addTest(TestFacultate('test_nume_facultate')) | |
suite.addTest(TestFacultate('test_numar_studenti')) | |
suite.addTest(TestFacultate('test_finantare')) | |
return suite | |
if __name__ == '__main__': | |
runner = unittest.TextTestRunner() | |
runner.run(suite()) |
In marea majoritate a cazurilor, apelul lui unittest.main() va determina colectarea tuturor instantelor de tip TestCase de la nivelul unui modul. Cu toate acestea, daca se doreste implementarea unui astfel de grup (suite) de teste, acest lucru poate fi realizat si manual.
Definitiile unitatilor de testare si grupurile de testare pot fi introduse si la nivelul modulelor ce introduc codul care urmeaza a fi testat, dar sunt o serie de avantaje cu privire la plasarea acestora in module separate:
• modulul de testare poate fi rulat individual din linia de comanda;
• codul de test trebuie modificat mult mai rar fata de codul pe care il evalueaza;
• codul de test poate fi mult mai usor separat de codul testat;
• daca strategia de testare se modifica, nu este necesara modificarea codului sursa.
Modulul Doctest
Modulul doctest identifica comentarii de documentare care seamana cu sesiuni interactive Python si le executa pentru a verifica daca codul este evaluat corect. Testele de tip doctest au o alta arie de utilizare fata de testele unitare.
def patrat(x): | |
"""Returneaza patratul lui x. | |
>>> patrat(2) | |
4 | |
>>> patrat(-2) | |
4 | |
""" | |
return x * x | |
if __name__ == '__main__': | |
import doctest | |
doctest.testmod() |
In general, testele de tip doctest sunt mult mai putin detaliate si nu au in vedere cazuri speciale. Sunt utilizate in postura de documentatie a principalelor cazuri de utilizare a unui modul sau a componentelor acestuia.
De exemplu, la rularea unui modul din linie de comanda se vor executa si testele din comentariile de documentare, si vor aparea mesaje corespunzatoare (failed), daca componentele testate nu se comporta in maniera prezentata la nivelul comentariului de documentare.
C:\Users\airman\virtualcampus\py>python -m doctest patrat.py
C:\Users\airman\virtualcampus\py>python -m doctest -v patrat.py Trying: patrat(2) Expecting: 4 ok Trying: patrat(-2) Expecting: 4 ok 1 items had no tests: patrat 1 items passed all tests: 2 tests in patrat.patrat 2 tests in 2 items. 2 passed and 0 failed. Test passed.