Išskirtinės situacijos
Kaip ir daugelis kitų šiuolaikinių programavimo kalbų, Pythonas turi išskirtines situacijas (angl. exceptions). Išskirtinės situacijos -- objektai, kuriuos galima pakelti ir galima sugauti. Standartinės Pythono funkcijos jas dažnai naudoja:
try: f = open('filename.txt') except IOError, e: print "Could not open filename.txt:", e
Jei bandymas atidaryti failą nepasiseka (pvz., to failo nėra, arba jis
nepasiekiamas), pakeliama IOError
tipo išskirtinė
situacija. try ... except
blokas ją pagauna ir
priskiria kintamajam e
.
Kai kurios standartinės išskirtinės situacijos (pilną sąrašą rasite Python dokumentacijoje):
Exception
- visų išskirtinių situacijų bazinė klasėIOError
- įvedimo/išvedimo klaida (nėra failo, nepavyko sukurti katalogo ir pan.)OSError
- operacinės sistemos klaidaIndexError
- sąrašo indeksas užeina už ribųKeyError
- žodyne nėra tokio raktoTypeError
- netinkamas argumento tipasValueError
- netinkama argumento reikšmėAttributeError
- objektas neturi tokio atributoNameError
- nėra tokio kintamojoSyntaxError
- sintaksės klaida Python programojeImportError
- nepavyko importuoti modulioZeroDivisionError
- dalyba iš nulioRuntimeError
- programos veikimo klaida (pvz., amžina rekursija)KeyboardInterrupt
- vartotojas paspaudė Ctrl+C
try ... except
bloku galima gaudyti daugiau nei
vieną išskirtinę situaciją:
try: do_something() except TypeError: print "TypeError" except KeyError: print "KeyError" except (NameError, AttributeError): print "NameError or AttributeError" except: print "any other error"
Taip pat matome, jog kintamojo vardą galime praleisti, jei nesiruošiame juo
naudotis. Jei vardiname kelis išskirtinių situacijų tipus, tą sąrašą reikia
apskliausti (kitaip kintamajam AttributeError
bus
priskiriamos NameError
tipo išskirtinės
situacijos).
Plikų except:
blokų reiktėtų vengti, nes jie
gali paslėpti programos klaidas. Galima vieną tokį bloką įdėti programos
pagrindiniame cikle ir, pavyzdžiui, parašyti apie įvykusią klaidą į log failą
(čia naudingas modulis traceback
).
Kartais mes nenorime apdoroti išskirtinių situacijų, bet norime atlaisvinti
resursus (pvz., uždaryti failus). Tam skirtas try ...
finally
blokas:
f = open('filename.txt') try: do_something() do_something_else() finally: f.close()
finally:
dalis bus vykdoma ir tuo atveju, jei
nekils išskirtinių situacijų, ir tuo atveju, jei jos kils.
Išskirtines situacijas kelti galima sakiniu raise
:
def fibonacci(n): if n < 1: raise ValueError('n must be >= 1, got %s' % n) elif n < 3: return 1 else: return fibonacci(n - 1) + fibonacci(n - 2)
Savo išskirtinių situacijų klases aprašyti nesudėtinga
class BadColour(ValueError): pass # ... def do_something_with_colout(colour): if not ...: raise BadColour(colour)
pass
reiškia, jog blokas yra tuščias. Blokas
gali būti bet koks -- klasės aprašo, funkcijos ar metodo aprašo, if sakinio,
else dalies, except dalies ir t.t.
Testavimas (unit testing)
Unit testai yra automatizuoti programų testai, tikrinantys kodo vienetus atskirai vieną nuo kito. Kiekvienas kodo vienetas (t.y. funkcija, klasė, metodas) turi savo testą ar kelis testus.
Pythono standartinė biblioteka turi modulį
unittest
, kuris pamėgdžioja Javos JUnit kodo
vienetų testų karkasą.
Pavyzdys:
""" mathfunc.py -- Matematinių funkcijų rinkinys """ def fibonacci(n): """Grąžina n-tąjį Fibonačio skaičių.""" if n < 1: raise ValueError(n) elif n <= 2: return 1 else: return fibonacci(n - 1) + fibonacci(n - 2) def factorial(n): """Grąžina skaičiaus n faktorialą.""" if n <= 1: return 1 else: return n * factorial(n - 1)
Testų rinkinys šiam moduliui
#!/usr/bin/env python """ Testų rinkinys moduliui mathfunc.py """ import unittest import mathfunc class TestMathFunc(unittest.TestCase): def test_factorial(self): f = mathfunc.factorial self.assertEquals(f(0), 1) self.assertEquals(f(1), 1) self.assertEquals(f(2), 2) self.assertEquals(f(3), 6) self.assertEquals(f(4), 24) self.assertEquals(f(5), 120) self.assertEquals(f(8), 40320) self.assertRaises(ValueError, f, -1) self.assertRaises(ValueError, f, -2) def test_fibonacci(self): fib = mathfunc.fibonacci self.assertEquals(fib(1), 1) self.assertEquals(fib(2), 1) self.assertEquals(fib(3), 2) self.assertEquals(fib(4), 3) self.assertEquals(fib(5), 5) self.assertEquals(fib(6), 8) self.assertEquals(fib(7), 13) self.assertRaises(ValueError, fib, 0) self.assertRaises(ValueError, fib, -1) self.assertRaises(ValueError, fib, -2) if __name__ == '__main__': unittest.main()
unittest.main
automatiškai susirenka visus testus
šiame modulyje. Testai atpažinami va kaip: randamos visos klasės, paveldinčios
iš unittest.TestCase
, ir išrenkami visi jų metodai,
kurių vardai prasideda "test". TestCase
klasė
turi rinkinį metodų, leidžiančių patikrinti kodo teisingumą:
- assertEquals(x, y) patikrina, ar x == y
- assertNotEquals(x, y) patikrina, ar x != y
- assert_(b) patikrina, ar reiškinys b yra teisingas
- assertRaises(e, f, a, b, c, ...) patikrina, ar funkcija f, iškviesta su argumentais a, b, c, ..., iškelia išskirtinę situaciją e.
Štai kaip galima testus paleisti (raktas -v prašo parodyti visų leidžiamų testų pavadinimus):
$ python test_mathfunc.py -v test_factorial (__main__.TestMathFunc) ... FAIL test_fibonacci (__main__.TestMathFunc) ... ok ====================================================================== FAIL: test_factorial (__main__.TestMathFunc) ---------------------------------------------------------------------- Traceback (most recent call last): File "test_mathfunc.py", line 21, in test_factorial self.assertRaises(ValueError, f, -1) File "/usr/lib/python2.3/unittest.py", line 295, in failUnlessRaises raise self.failureException, excName AssertionError: ValueError ---------------------------------------------------------------------- Ran 2 tests in 0.006s FAILED (failures=1)
Matome, jog programoje yra klaida -- factorial
funkcija nekelia ValueError
klaidos gavusi neigiamą
argumentą. Ištaisę klaidą vėl paleisime testus ir įsitikinsime, kad klaida
tikrai ištaisyta.
Suradus programoje klaidą, kurios nepagauna testai, visų prima reikėtų sukurti naują testą, kuris tą klaidą pagautų. Tokiu būdu būsime tikri, kad ateityje ta pati klaida nepasikartos. Tai vadinama regresiniu testavimu (angl. regression testing).
Testavimo nauda
E. W. Dijkstra yra pasakęs, kad testai gali įrodyti programos klaidingumą, bet negali įrodyti jos teisingumo. Ar tai reiškia, kad testų rašyti neapsimoka? Ne. Testai padeda įsitikinti, kad keisdami programą jos netyčia nesugadinome. Testų rašymas dažnai sugauna žioplas klaidas, kurias padaro pavargęs programuotojas. Iš kitos pusės reikia jausti saiką -- jei funkcija tokia triviali, kad joje tikrai klaidų būti negali, nėra prasmės rašyti jai testo -- geriau tą sutaupytą laiką sunaudoti rašant testą sudėtingai funkcijai.
Pythono programuotojų bendruomenėje testai dažnai siūlomi kaip atsakas statiškai tipizuotoms kalboms. Testų rinkinys sugauna klaidas, kurias kitose programavimo kalbose sugautų kompiliatoriau atliekamas tipų patikrinimas. O taip pat sugauna ir kitas klaidas, kurių statiškai tipizuotų klaidų kompiliatoriai nepastebi. Geriau investuoti laiką rašant testus, negu rašinėjant tipų deklaracijas kiekvienai funkcijai, kintamajam ar metodui.
Egzistuoja disciplina, vadinama "testai pirma" (angl. "tests first" arba "test driven development (TDD)"):
- Rašomas testas
- Rašomas kodas
- Leidžiami testai (jei jie nepraeina, kodas taisomas, kol jie praeina)
- Kodas tvarkomas, kad būtų gražus ir lengvai suprantamas
- Leidžiami testai (įsitikiname, kad gražindami kodą nieko nesugriovėme)
- Ciklas kartojamas iš pradžių
Tokio darbo būdo nauda:
- Turime pilną testų rinkinį
- Kodo dizainą įtakoja tai, kaip kodas bus naudojamas, o ne tai, kaip jį patogiau realizuoti.
- Akcentuojamas kodo paprastumas
TDD yra viena iš sudėtinių ekstremalaus programavimo (XP) dalių.
Ne viską galima lengvai testuoti. Sritys, kuriose sudėtinga kurti automatizuotus testus yra:
- Vartotojo sąsajos programavimas
- Paskirstytos sistemos
Tokiais atvejais geriausia visą programos logiką turėti testuojamuose komponentuose, o vartotojo sąsają ar ryšius su kitomis paskirstytos sistemos dalimis laikyti kiek galima „plonesnius“.
Dokumentacijos testai
Paprastų funkcijų testus galima užrašyti tų funkcijų dokumentacijos eilutėse, kaip vartojimo pavyzdžius:
def abs(x): """Grąžina |x|, t.y. skaičiaus x absoliučiąją reikšmę. Pavyzdžiai: >>> abs(1) 1 >>> abs(-2) 2 >>> abs(0) 0 """ if x >= 0: return x else: return -x
Tokį dokumentacinį testą galima nukopijuoti tiesiai iš interaktyvios Python sesijos.
Dokumentaciniams testams leisti ir tikrinti yra skirtas doctest modulis, tačiau didesnėje programoje geriau juos integruoti į testų modulius, kartu su kitais testais. Štai kaip tai daroma:
#!/usr/bin/env python """ Testų rinkinys moduliui mathfunc.py """ import unittest import doctest import mathfunc class TestMathFunc(unittest.TestCase): ... ... ... def test_suite(): suite = unittest.TestSuite() suite.addTest(unittest.makeSuite(TestMathFunc)) suite.addTest(doctest.DocTestSuite(mathfunc)) return suite if __name__ == '__main__': unittest.main(defaultTest='test_suite')
Skirtumai nuo ankstesniojo testų modulio:
- Importuojame
doctest
. - Aprašome funkciją
test_suite
. - Šioje funkcijoje sudedame visas testų klases į rinkinį, o taip pat į
testų rinkinį įtraukiame
DocTestSuite
norimam moduliui. unittest.main
perduodame argumentądefaultTest
.
Keli testų failai
Didesnėje programoje bus daug Python kodo modulių, ir ne mažiau testų modulių. Kad nereiktų kiekvieno testų modulio rankutėmis paleidinėti, galime pasinaudoti skriptu, kuris tai padarys už mus:
#!/usr/bin/python """Skriptukas, paleidžiantis visus programos testus.""" import os import unittest def test_files(): """Grąžina sąrašą su visais testų failais.""" files = [] for dirpath, dirnames, filenames in os.walk('.'): files += [f for f in filenames if f.startswith('test') and f.endswith('.py')] return files def test_modules(test_files): """Paverčia failų vardus į modulių vardus.""" return [filename[:-3].replace(os.path.sep, '.') for filename in test_files] def main(): suite = unittest.TestSuite() for module_name in test_modules(test_files()): module = __import__(module_name, {}, {}, ()) suite.addTest(module.test_suite()) unittest.TextTestRunner().run(suite) if __name__ == '__main__': main()
Ši programėlė tikisi kiekviename testų modulyje surasti funkciją
test_suite
, panašiai kaip praeitame skyrelyje
buvusiame pavyzdyje.
Sudėtingesni testai
Testų klasėse galima aprašyti metodus setUp
ir
tearDown
, kurie bus automatiškai paleidžiami
atitinkamai prieš kiekvieną testą ir po kiekvieno testo. Šiuose metoduose
galite susikurti aplinką, reikiamą testams.
Pavyzdys: testuojame metodą, kuris sukuria kataloge tris failus. setUp
metode susikursime laikiną katalogą, o tearDown
modulyje jį ištrinsime, kad mūsų testai
nepalikinėtų po savęs šiukšlių.
import unittest import tempfile import shutil ... class TestFileCreator(unittest.TestSuite): def setUp(self): self.dir = tempfile.mkdtemp() def test(self): create_three_files(dir=self.dir) files = os.listdir(self.dir) self.assertEquals(len(files), 3) def tearDown(self): shutil.rmtree(self.dir) ...