Back to site
Since 2004, our University project has become the Internet's most widespread web hosting directory. Here we like to talk a lot about web servers, web development, networking and security services. It is, after all, our expertise. To make things better we've launched this science section with the free access to educational resources and important scientific material translated to different languages.

Pregled Python Unit Testing Framework

Python okvir za testiranje jedinica, koji je dobio naziv 'PyUnit' prema konvenciji, predstavlja verziju Python jezika za JUnit, čiji su tvorci pametnice Kent Beck i Erich Gamma. JUnit je nastavak Java verzije Kent-ovog okvira za testiranje pod nazivom Smalltalk. Svaki od njih predstavlja de facto okvir za testiranje određenog jezika.

U ovom tekstu data su objašnjenja specifičnih aspekata dizajna i upotrebe PyUnit-a za Python; za informacije o osnovnom dizajnu okvira, čitalac se upućuje na Kent-ov originalni rad, "Simple Smalltalk Testing: With Patterns".

PyUnit čini deo Python standardne biblioteke, kao Python verzija 2.1.

Informacije koje slede podrazumevaju poznavanje Python jezika koji je tako lak da sam čak i ja uspeo da ga naučim i izaziva toliku zavisnost da sad ne mogu prestati da ga koristim.

Sistemski zahtevi

PyUnit je dizajniran za rad s bilo kojim standardnim Python-om, verzije 1.5.2 ili novije.

PyUnit je testiran od strane autora Linux-a (Redhat 6.0 i 6.1, Debian Potato) uz Python 1.5.2, 2.0 i 2.1. Takođe, poznato je da radi i na drugim Python patformama, uključujući Windows i Mac. Ako vam bio koja platforma ili Python verzija zadaje probleme, obavestite me.

Za pojedinosti o upotrebi PyUnit-a uz JPython i Jython, pogledajte odeljak 'Upotreba PyUnit-a uz JPython i Jython'.

Upotreba PyUnit-a za pisanje sopstvenih testova

Instalacija

Klase neophodne za pisanje testova mogu se naći u modulu 'unittest'. Ovaj modul je deo standardne Python biblioteke za Python 2.1 i kasniju verziju. Ako koristite stariju Python verziju, treba da uzmete modul iz odvojene PyUnit distribucije.

Da biste koristili modul iz sopstvenog koda, jednostavno se pobrinite za to da se u vašoj putanji za pretragu nađe direktorijum koji sadrži datoteku 'unittest.py'. Možete to uraditi postavljanjem '$PYTHONPATH' promenljive okruženja ili tako što ćete staviti datoteku u direktorijum na vašoj trenutnoj Python putanji za pretragu, kao što je /usr/lib/python1.5/site-packages na Redhat Linux mašinama.

Imajte u vidu da ćete to morati da uradite pre nego što pokrenete primere u kojima se nalazi PyUnit, osim ako ne kopirate 'unittest.py' u direktorijum sa primerima.

Uvod u primere testiranja

Osnovne kockice testiranja jedinica čine 'primeri testiranja' -- pojedinačni scenariji koji se moraju podesiti i čija se tačnost mora proveriti. U PyUnit-u, primeri testiranja predstavljeni su klasi TestCase u modulu unittest. Da biste napravili sopstvene primere za testiranje, morate napisati podklase za TestCase.

Primer za klasu TestCase je objekat koji može kompletno izvesti jedan metod testiranja, zajedno sa opcionim postavljanjem i čišćenjem koda.

Kod za testiranje TestCase primera treba da bude potpuno samostalan, takav da se može pokrenuti bilo izolovano, bilo u kombinaciji s proizvoljnim brojem drugih primera testiranja.

Kreiranje jednostavnog primera za testiranje

Najjednostavnija podklasa primera za testiranje prosto će zameniti runTest metod da bi se izvršilo specifično testiranje koda.

        import unittest

        class DefaultWidgetSizeTestCase(unittest.TestCase):
            def runTest(self):
                widget = Widget("The widget")
                assert widget.size() == (50,50), 'incorrect default size'
    

Zapamtite, da bismo nešto testirali, koristimo ugrađenu Python naredbu 'assert'. Ako naredba ne uspe kad se pokrene test, pojavljuje se AssertionError, a okvir za testiranje identifikovaće primer kao “neuspeh”. Drugi izuzeci, koji ne proizilaze iz eksplicitne naredbe, okvir za testiranje identifikuje kao “greške”. (Pogledajte odeljak: Više informacija o uslovima testiranja.)

Kasnije ćemo opisati kako se pokreće primer za testiranje. Za sada, da bismo konstruisali primer za test, pozivamo njegovog konstruktora bez argumenata:

        testCase = DefaultWidgetSizeTestCase()
    

Ponovna upotreba postavki koda: stvaranje 'fikstura'

Pošto primeri za testiranje mogu biti brojni, njihova postavka može se ponavljati. U pomenutom slučaju, konstruisanje 'Dodatka' u svakom od 100 primera testiranja podklasa dodataka podrazumevalo bi neugledno dupiranje.

Srećom, možemo uzeti u obzir tako postavljen kod tako što ćemo primeniti hook metod nazvan setUp, koji će nas automatski pozvati kad pokrenemo test:

        import unittest

        class SimpleWidgetTestCase(unittest.TestCase):
            def setUp(self):
                self.widget = Widget("The widget")

        class DefaultWidgetSizeTestCase(SimpleWidgetTestCase):
            def runTest(self):
                assert self.widget.size() == (50,50), 'incorrect default size'

        class WidgetResizeTestCase(SimpleWidgetTestCase):
            def runTest(self):
                self.widget.resize(100,150)
                assert self.widget.size() == (100,150), \
                       'wrong size after resize'
    

Ako setUp metod otkrije izuzetak u toku testiranja, okvir će smatrati da je došlo do greške u testiranju i neće pokrenuti Test metod.

Slično tome, možemo obezbediti metod tearDown koji čisti posle izvršenja metoda runTest:

        import unittest

        class SimpleWidgetTestCase(unittest.TestCase):
            def setUp(self):
                self.widget = Widget("The widget")
            def tearDown(self):
                self.widget.dispose()
                self.widget = None
    

Ako je postavka uspela, tearDown metod pokrenuće se bez obzira na to da li je runTest metod uspeo ili ne.

Takvo radno okruženje za testiranje koda naziva se fikstura.

Klase TestCase s raznim metodama testiranja

Često mnogi manji primeri za testiranje koriste isto radno okruženje. U tom sučaju, razdelili bismo SimpleWidgetTestCase u mnogo malih podklasa koje se sastoje od klasa sa po jednim metodom, kakva je DefaultWidgetSizeTestCase. Pošto je to dugotrajan i obeshrabrujući proces, a po stilu je isti kao JUnit, PyUnit nudi jednostavniji mehanizam:

        import unittest

        class WidgetTestCase(unittest.TestCase):
            def setUp(self):
                self.widget = Widget("The widget")
            def tearDown(self):
                self.widget.dispose()
                self.widget = None
            def testDefaultSize(self):
                assert self.widget.size() == (50,50), 'incorrect default size'
            def testResize(self):
                self.widget.resize(100,150)
                assert self.widget.size() == (100,150), \
                       'wrong size after resize'
    

Ovde nismo obezbedili runTest metod, ali umesto njega obezbedili smo dva različita metoda testiranja. Svaki primer klasa pokrenuće po jedan test metod, koristeći self.widget kreiran i uništen za svaki primer pojedinačno. Kad kreiramo primer, moramo odrediti i metod testiranja koji će biti pokrenut. To radimo tako što naziv metoda unosimo u konstruktor:

        defaultSizeTestCase = WidgetTestCase("testDefaultSize")
        resizeTestCase = WidgetTestCase("testResize")
    

Sakupljanje primera testiranja u test paket (Test Suite)

Pojedinačni primeri testiranja grupišu se prema karakteristikama koje se testiraju. PyUnit obezbeđuje mehanizam za to: 'test suite', koji predstavlja klasa TestSuite u unittest modulu:

        widgetTestSuite = unittest.TestSuite()
        widgetTestSuite.addTest(WidgetTestCase("testDefaultSize"))
        widgetTestSuite.addTest(WidgetTestCase("testResize"))
    

Zbog lakšeg pokretanja testova, kao što ćemo kasnije videti, dobro je obezbediti objekat koji se može pozvati u svakom modulu testiranja, objekat koji vraća test paket u prethodno stanje:

       def suite():
           suite = unittest.TestSuite()
           suite.addTest(WidgetTestCase("testDefaultSize"))
           suite.addTest(WidgetTestCase("testResize"))
           return suite
    

ili čak:

       class WidgetTestSuite(unittest.TestSuite):
           def __init__(self):
               unittest.TestSuite.__init__(self,map(WidgetTestCase,
                                                     ("testDefaultSize",
                                                      "testResize")))
    

(doduše, ovaj drugi nije za kukavice)

Pošto je ustaljeni obrazac kreiranja TestCase podklasa s mnogim testnim funkcijama sličnih naziva, zbog lakšeg snalaženja, osmišljena je funkcija pod nazivom makeSuite u unittest modulu, koji čini testni paket sastavljen od svih slučajeva testiranja u klasi test case:-

       suite = unittest.makeSuite(WidgetTestCase,'test')
    

Zapamtite: kad koristite funkciju makeSuite, redosled kojim će test paket pokretati razne primere testiranja određen je sortiranjem naziva funkcija uz pomoć ugrađene cmp funkcije.

Gnežđenje test paketa

Obično je poželjno grupisati pakete primera za testiranje da bi se odjednom pokrenulo testiranje celokupnog sistema. To je jednostavno, pošto se TestSuites mogu dodati u TestSuite, baš kao što se TestCases mogu dodati u TestSuite:-

       suite1 = module1.TheTestSuite()
       suite2 = module2.TheTestSuite()
       alltests = unittest.TestSuite((suite1, suite2))
    

Primer za gnežđenje test paketa može se naći u datoteci 'alltests.py', u poddirektorijumu 'examples' distribucionog paketa.

Gde smestiti kod za testiranje

Možete smestiti definicije test primera i test paketa u iste module pošto treba da testiraju isti kod (npr. 'widget.py'), ali postoji nekoliko prednosti u tome da se test kod smesti u odvojeni modul, kao što je 'widgettests.py':

Interaktivno pokretanje testova

Naravno, svrha pisanja ovih testova je njihovo pokretanje i rešavanje pitanja da li naš softver radi. Okvir za testiranje koristi klase 'TestRunner' za obezbeđivanje okruženja u kome se može sprovesti vaše testiranje. Najčešći TestRunner je TextTestRunner, koji pokreće testove i izveštava o rezultatima u tekstualnom obliku:

        runner = unittest.TextTestRunner()
        runner.run(widgetTestSuite)
    

TextTestRunner je podešen da štampa rezultat u sys.stderr, ali to se može promeniti prenosom nekog drugog objekta, poput datoteke, njegovom konstruktoru.

Upotreba TextTestRunner-a je idealan način za interaktivno pokretanje vaših testova iz sesije Python prevodioca.

Pokretanje testova iz komandne linije

Modul unittest sadrži funkciju main, koja se koristi za prebacivanje test modula u skripta koja će pokrenuti test sadržan u njima. Funkcija main koristi unittest.TestLoader klasu da bi automatski pronalazila i unosila primere za testiranje u tekućem modulu.

Ako svoje test metode nazovete ranije opisanom test* konvencijom, možete sledeći kod smestiti na dno svog modula za testiranje:

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

Zatim, kad pokrenete test modul s komandne linije, pokrenuće se svi testovi koji se nalaze u modulu.

Da biste pokrenuli proizvoljne testove sa komandne linije, možete pokrenuti unittest modul kao skripta tako što ćete ih nazvati prema primerima ili paketima testova:

        % python unittest.py widgettests.WidgetTestSuite
    

ili

        % python unittest.py widgettests.makeWidgetTestSuite
    

Možete, takođe, izabrati određene testove na komandnoj liniji. Da biste pokrenuli TestCase podklasu 'ListTestCase' u modulu 'listtests' (pogledajte poddirektorijum 'examples' u distribucionom paketu), možete da izvršite komandu:

        % python unittest.py listtests.ListTestCase.testAppend
    

gde je 'testAppend' naziv metoda testa koji treba da pokrene primer za testiranje. Da biste kreirali i pokrenuli ListTestCase primere za sve 'test*' metode u toj klasi, možete pokrenuti:

        % python unittest.py listtests.ListTestCase
    

GUI pokretač testa

Postoji i grafički front end koji možete koristiti za pokretanje testova. Za njegovo pisanje upotrebljen je Tkinter, alat sa prozorima koji se isporučuje uz Python na većini platformi. Liči na JUnit GUI.

Da biste koristili GUI pokretač testa, jednostavno pokrenite:

        % python unittestgui.py
    

ili

        % python unittestgui.py widgettests.WidgetTestSuite
    

Imajte na umu da ovde, opet, uneto ime za test koji se sprovodi treba da bude potpuno kvalifikovan naziv objekta koji vraća TestCase ili TestSuite instancu. Ne treba to da bude naziv prethodno kreiranog testa, pošto svaki test mora ponovo da se kreira kad god se pokrene.

Korišćenje GUI pokretača testa umesto tekstualnog pokretača testa produžava vreme testiranja zbog svih ažuriranja prozora. Na mom sistemu, zahteva 7 dodatnih sekundi za svakih hiljadu testova. Vaša kilometraža može da varira.

Dokumentovanje vaših testova

Obično, kad se test pokrene, TestRunner prikazuje njegov naziv. Taj naziv je izveden od naziva klase primera testa i naziva metoda testiranja pokrenutog za određeni primer.

Međutim, ako unesete doc-niz za metod testiranja, prvi red tog niza biće prikazan kad se pokrene test. Zahvaljujući tome, obezbeđen je jednostavan mehanizam za dokumentovanje vaših testova:

        class WidgetTestCase(unittest.TestCase):
            def testDefaultSize(self):
                """Check that widgets are created with correct default size"""
                assert self.widget.size() == (50,50), 'incorrect default size'
    

Više o uslovima testiranja

Predlažem da koristite Python-ov ugrađeni mehanizam za proveru uslova u testiranim primerima umesto nekog “ručno rađenog” ekvivalenta; assert je jednostavan, koncizan i poznat.

Imajte na umu da, ako su testovi pokrenuti sa uključenom Python-ovom optimizacijom (generišu '.pyo' bytecode datoteke), assert komande će biti preskočene, a test cases bespotrebne.

Za one koji vole da rade sa omogućenom opcijom optimizacije, uključio sam metod assert_ u klasu TestCase. Po funkcionalnosti, jednak je ugrađenoj assert funkciji i neće je nadmašiti, ali je manje prikladan i rezultira porukama koje izveštavaju o greškama, ali nisu od velike koristi.

        def runTest(self):
            self.assert_(self.widget.size() == (100,100), "size is wrong")
    

Za svaki slučaj, obezbedio sam TestCase sa failIf i failUnless metodama:

        def runTest(self):
            self.failIf(self.widget.size() <> (100,100))
    

Test case metod takođe može pozvati fail da bi se odmah okončao:

        def runTest(self):
            ...
            if not hasattr(something, "blah"):
                self.fail("blah missing")
                # or just 'self.fail()'
    

Testiranje za ravnopravnost

Najčešći tip tvrdnji je potvrda o jednakosti između dve vrednosti ili dva objekata. Ako tvrdnja ne uspe, programer obično želi da vidi koja je, zapravo, vrednost bila pogrešna.

Za tu svrhu, TestCase ima par metoda pod nazivom assertEqual i assertNotEqual (sa alijasima failUnlessEqual i failIfEqual za one koji više vole taj način):

        def testSomething(self):
            self.widget.resize(100,100)
            self.assertEqual(self.widget.size, (100,100))
    

Testiranje za izuzetke

Često ćete testiranjem želeti da proverite da i je izuzetak prerastao u skup okolnosti. Ako očekivani izuzetak nije prikazan, test treba smatrati neuspešnim. To je lako:

        def runTest(self):
            try:
                self.widget.resize(-1,-1)
            except ValueError:
                pass
            else:
                fail("expected a ValueError")
    

Obično je izvor očekivanog izuzetka objekat koji se može pozvati. Zbog toga TestCase ima assertRaises metod. Prva dva argumenta metoda predstavljaju očekivani izuzetak, onako kako bi se pojavljivao u 'except' klauzi i objekat koji se može pozvati. Preostale argumente treba preneti objektu koji se može pozvati:

        def runTest(self):
            self.assertRaises(ValueError, self.widget.resize, -1, -1)
    

Ponovna upotreba starog test koda uz PyUnit

Neki korisnici otkriće da imaju postojeći test kod koji žele da pokrenu iz PyUnit-a, bez konvertovanja svake stare testne funkcije u TestCase podklasu.

Zbog toga PyUnit obezbeđuje klasu FunctionTestCase. Ova podklasa klase TestCase može se upotrebiti kao omotač postojeće test funkcije. Funkcije Set-up i tear-down mogu se, takođe, umotati prema potrebi.

Imajući u vidu sledeću test funkciju:

        def testSomething():
            something = makeSomething()
            assert something.name is not None
            ...
    

moguće je kreirati ekvivalent primera testa na sledeći način:

        testcase = unittest.FunctionTestCase(testSomething)
    

Ako treba pozvati dodatne set-up i tear-down metode, kao deo operacije testiranja primera, mogu se obezbediti:

        testcase = unittest.FunctionTestCase(testSomething,
                                             setUp=makeSomethingDB,
                                             tearDown=deleteSomethingDB)
    

Upotreba PyUnit-a uz JPython i Jython

Iako je PyUnit primarno napisan za 'C' Python, PyUnit testovi mogu se napisati i Jython-om za vaš Java ili Jython softver. To može biti bolje od pokušaja pisanja JUnit testova uz pomoć Jython-a. PyUnit takođe dobro radi sa Jython-ovim prethodnicima: JPython 1.0 i 1.1.

Naravno, Java nema TK GUI interfejs, pa PyUnit-ov GUI na bazi Tkinter-a neće raditi sa Jython-om. Međutim, tekstualni interfejs radi sasvim dobro.

Da biste to uradili, jednostavno kopirajte datoteke modula standardne C Python biblioteke: 'traceback.py', 'linecache.py', 'stat.py' i 'getopt.py' na lokaciju odakle ih JPython može uvesti. Te datoteke možete dobiti iz bilo koje distribucije C Python-a. (Ove smernice zasnivaju se na standardnoj biblioteci C Python 1.5.x i možda nisu tačne za ostale Python verzije.)

Sad možete napisati svoje PyUnit testove na isti način kao što biste to uradili koristeći C Python.

Upozorenja

Tvrdnje

Pogedajte upozorenja u odeljku "Više o uslovima testiranja" gore.

Upotreba memorije

Kad se pojave izuzeci tokom testiranja, rezultirajući spisak objekata čuva se tako da detallji o neuspehu mogu da se formatiraju i štampaju na kraju testiranja. Osim jednostavnosti, dobra strana toga je što će buduće verzije GUI TestRunner-a omogućavati post-mortem inspekciju lokalnih i globalnih vrednosti promenljivih koje se čuvaju na listi.

Mogući sporedni efekat je činjenica da veoma velika stopa neuspelih testova zauzima mnogo memorije sa svojim listama objekata što može postati problem. Naravno, ako vam je toliko testova doživelo neuspeh, preopterećenje memorije vam je najmanji problem.

Uslovi korišćenja

Možete slobodno koristiti, menjati i distribuirati ovaj softver pod istim liberalnim uslovima koji važe za sam Pithon. Sve što tražim je da moje ime, e-mail adresa i URL-projekta budu sačuvani u izvornom kodu i u pratećoj dokumentaciji i da se moje ime navede kao ime originalnog autora.

Motiv za pisanje ovog softvera mi je skroman doprinos poboljšanju kvaliteta softvera u svetu, nisam se pogodio ni za kakav novac. (To ne znači da sponzorstvo neće biti dobrodošlo.)

Planovi za budućnost

Ključni plan za budućnost je da se integrišu TK GUI i IDLE IDE. Volonteri su dobrodoši!

Osim toga, nemam velike planove za proširenje funkcionalnosti modula. Održavam PyUnit tako da bude što je moguće jednostavniji (ali ne jednostavniji od toga, nadam se!) jer verujem da pisci testova bolje od mene pišu pomoćne module za ovako uobičajena testiranja kao što su poređenja log datoteka.

Ažuriranje i zajednica

Vesti, ažuriranje i još mnogo toga možete naći na web site-u projekta.

Komentari, predozi i izveštaji o greškama su dobrodošli; samo mi pošaljite mail ili, još bolje, priključite se veoma oskudnoj mailing listi i tu postavite svoje komentare. Iznenađujuće veliki broj ljudi već koristi PyUnit i svi oni mogu podeliti svoju mudrost.

Zahvalnost

Guido i njegovi učenici zaslužuju zahvalnost za Python jezik. U njihovu čast, napisao sam sledeći haiku (ili 'pyku - Python GUI Kit for Haiku', ako vam je to draže):

Sa zahvalnošću posmatram šta su Kent Beck i Erich Gamma uradili na JUnit-ovom dizajnu koji ne zahteva nikakvo mozganje.

Tim Voght, takođe, zaslužuje moju zahvalnost. Kad sam implementirao PyUnit, otkrio sam da je i on implementirao 'pyunit' modul kao deo svog 'PyWiki' WikiWikiWeb klona. Graciozno je podržao moj rad tako što je moju verziju podneo na uvid celokupnoj zajednici.

Mnogo hvala svima koji su mi poslali sugestije i pitanja. Pokušao sam da dodam odgovarajuće zasluge u datoteci CHANGES u paketu za preuzimanje.

Jérôme Marant, koji je spakovao PyUnit za Debian ima moju posebnu zahvalnost.





Published (Last edited): 06-06-2013 , source: http://pyunit.sourceforge.net/pyunit.html