Language
Lt :: En

Trečioji paskaita (2004-09-20)

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):

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ą:

Š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)"):

  1. Rašomas testas
  2. Rašomas kodas
  3. Leidžiami testai (jei jie nepraeina, kodas taisomas, kol jie praeina)
  4. Kodas tvarkomas, kad būtų gražus ir lengvai suprantamas
  5. Leidžiami testai (įsitikiname, kad gražindami kodą nieko nesugriovėme)
  6. Ciklas kartojamas iš pradžių

Tokio darbo būdo nauda:

TDD yra viena iš sudėtinių ekstremalaus programavimo (XP) dalių.

Ne viską galima lengvai testuoti. Sritys, kuriose sudėtinga kurti automatizuotus testus yra:

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:

  1. Importuojame doctest.
  2. Aprašome funkciją test_suite.
  3. Šioje funkcijoje sudedame visas testų klases į rinkinį, o taip pat į testų rinkinį įtraukiame DocTestSuite norimam moduliui.
  4. 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)

...


Valid XHTML 1.1! Valid CSS! Paskutiniai pakeitimai: 2012-01-08