Testiranje pravilnosti kode, uporabniški vmesnik

Testiranje pravilnosti kode

Kadar razvijamo bolj obsežno kodo in pri tem sodeluje več oseb, se pojavi potreba po avtomatskem testiranju pravilnosti kode. To pomeni, da poleg kode, ki ima določen rezultat, definiramo tudi testirne funkcije; slednje preverjajo ali prve vračajo pričakovani rezultat.

Preprosti primer je:

# vsebina datoteke test_prvi.py (mapa moduli)
def kvadrat(x):
    return x**2 + 1 # očitna napaka glede na ime funkcije

def test_kvadrat():
    assert kvadrat(2) == 4

Če poženemo funkcijo test_kvadrat() se testira pravilnost kvadrat(2) == 4; v primeru, da je rezultat True se ne zgodi nič, če pa je rezultat False se sproži AssertionError() (glejte dokumentacijo za ukaz assert). Ker funkcija kvadrat() vrne x**2+1, bo testiranje neuspešno.

Verjetno se sprašujete v čem je smisel takega testiranja pravilnosti. Ko koda postaja obsežna in na njej dela veliko oseb, postane nujno tudi prepletena. Tako se lahko zgodi, da razvijalec doda novo funkcionalnost in nehote poruši obstoječo. Če so vse funkcionalnosti testirane, bo testiranje zaznalo neustreznost predlagane spremembe.

Večina paketov ima tako dodano testiranje pravilnosti; za primer si lahko pogledate izvorno kodo paketa NumPy, ki se nahaja na githubu; če pogledate vsebino podmodula numpy.linalg za linearno algebro:

opazimo podmapo tests. Slednja je v celoti namenjena testiranju in vsebuje množico Python datotek, ki preverjajo ustreznost podmodula.

Obstaja več možnosti testiranja kode, pogosto se uporabljajo sledeče:

Vsi trije pristopi imajo podobno funkcionalnost. V zadnjem obdobju pa se najpogosteje uporablja pytest (npr. NumPy/SciPy), katerega osnove si bomo tukaj pogledali.

pytest

Modul pytest je vključen v Anaconda distribucijo Pythona, sicer pa ga namestimo z ukazom pip ali conda.

Nekatere lastnosti:

  • vrne opis testa, ki ni bil uspešen,
  • samodejno iskanje testnih modulov in funkcij,
  • lahko vključuje unittest in nose teste.

Če v ukazni vrstici poženete pytest, bo program sam poiskal trenutno mapo in vse podmape za datoteke oblike test_*.py ali *_test.py (napredno iskanje je navedeno v dokumentaciji).

Če v ukazni vrstici poženemo ukaz:

pytest

bomo dobili tako poročilo:

============================= test session starts =============================
platform win32 -- Python 3.6.2, pytest-3.2.1, py-1.4.34, pluggy-0.4.0
rootdir: c:\pypinm\moduli, inifile:
collected 4 items

test_orodja.py ...
test_prvi.py F

================================== FAILURES ===================================
________________________________ test_kvadrat _________________________________

    def test_kvadrat():
>       assert kvadrat(2) == 4
E       assert 5 == 4
E        +  where 5 = kvadrat(2)

test_prvi.py:5: AssertionError
===================== 1 failed, 3 passed in 0.65 seconds ======================

Iz poročila vidimo, da je program našel dve datoteki (test_orodja.py in test_prvi.py) in da je prišlo do napake pri eni funkciji (test_kvadrat), tri funkcije pa so uspešno prestale test.

Za osnovno uporabo pri numeričnih izračunih je treba še izpostaviti modul numpy.testing (dokumentacija), ki nudi podporo za testiranje numeričnih polj.

Izbrane funkcije so:

  • assert_allclose(dejansko, pričakovano[, rtol, ...]) (dokumentacija) preveri enakost do zahtevane natančnosti,
  • assert_array_less(x, y[, err_msg, verbose]) (dokumentacija) preveri, ali so elementi x manjši od elementov y,
  • assert_string_equal(dejansko, pričakovano) (dokumentacija) preveri enakost niza.

Zgled: test_orodja.py:

V datoteki test_orodja.py so pripravljene funkcije za testiranje modula orodja.py.

Tako so najprej pripravljeni podatki:

zacetna = np.asarray([[1, 2, 3],
                      [4, 5, 6],
                      [7, 8, 9]])
zamenjana_0_1_stolpca = np.asarray([[2, 1, 3],
                                    [5, 4, 6],
                                    [8, 7, 9]])

Potem je definirana funkcija, ki kliče funkcijo orodja.zamenjaj_stolpca() in rezultat primerja s pričakovanim zamenjana_0_1_stolpca:

def test_stolpec():
    a = zacetna.copy() # naredimo kopijo podatkov
    b = orodja.zamenjaj_stolpca(a, 0, 1) # b (in tudi a) imata zamenjane stolpce
    np.testing.assert_allclose(b, zamenjana_0_1_stolpca)

Če funkcija orodja.zamenjaj_stolpca() deluje pravilno, se klicanje pytest v ukazni vrstici uspešno zaključi. Za preostale teste poglejte datoteko test_orodja.py in dokumentacijo paketa pytest.

Grafični uporabniški vmesnik

Uporabniški vmesnik se uporablja za interakcijo uporabnika s programsko kodo. Uporabniški vmesniki so lahko preko ukazne vrstice ali grafični. Za preproste uporabniške vmesnike preko ukazne vrstice je najbolje, da uporabimo kar argparse, sicer pa se bomo tukaj predvsem osredotočili na grafični uporabniški vmesnik.

Grafični uporabniški vmesnik znotraj brskalnika

V zadnjem obdobju je opaziti veliko napora pri podpori razvoja grafičnih vmesnikov znotraj okolja Jupyter notebook ali Jupyter lab. Takšen pristop ima predvsem to prednost, da je nadgradnja v obliki spletne aplikacije relativno enostavna, glejte npr.: Dashboarding with Jupyter Notebooks, Voila and Widgets | SciPy 2019 | M. Breddels and M. Renou.

Grafični uporabniški vmesnik znotraj operacijskega sistema

v okviru tega poglavja si bomo pogledali klasične uporabniške vmesnike, ki jih poganjamo v okviru določenega operacijskega sistema. Tudi za programiranje grafičnega uporabniškega vmesnika obstaja veliko različnih načinov/modulov. Nekaj najpogosteje uporabljenih:

  • PyQt temelji na Qt, ki predstavlja najbolj razširjeno platforma za uporabniške vmesnike,
  • PySide podobno kakor PyQt, vendar s širšo licenco LGPL, a žal tudi slabšo podporo (tukaj je dobra knjiga: PySide GUI Application Development - SE),
  • Kivy za hiter razvoj modernih uporabniških vmesnikov (ni tako zrel kakor npr. Qt),
  • wxWidgets široko uporabljena prosta platforma za izdelavo uporabniških vmesnikov.

Med vsemi naštetimi si bomo podrobneje pogledali PyQt, ki je verjetno najboljša in tudi najbolj dozorela izbira (za komercialno uporabo pa žal ni brezplačna).

Velja omeniti, da uporabniški vmesnik lahko:

Tukaj bomo uporabniški vmesnik kodirali.

Zgled

Najprej uvozimo paket za interakcijo in z grafičnimi objekti:

In [1]:
from PyQt5 import QtCore     # za interakcijo 
from PyQt5 import QtWidgets  # grafični objekti

Potrebovali bomo tudi modul sys za poganjanje programa:

In [2]:
import sys

Uporabniški vmesnik tipično gradimo na razredu QtWidgets.QMainWindow (dokumentacija), ki ima spodaj prikazano strukturo:

Bistveni grafični elementi, ki jih pri takem uporabniškem vmesniku uporabimo, so:

  • Menu Bar,
  • Toolbars,
  • Dock Widgets,
  • Central Widget,
  • Status bar.

Ni potrebno definirati vseh; spodaj si bomo pogledali primer, ko bomo definirali Status bar, Central Widget in Menu Bar. Ponavadi vse elemente definiramo pri inicializaciji instance razreda (metoda __init__).

Tukaj izpostavimo, da grafični vmesnik temelji na t. i. Widgetih, (za na primer gumb, tabelo, datum itd.). Prikaz nekaterih možnosti je prikazan v:

Zelo enostaven uporabniški vmesnik (z vrstičnimi komentarji kode) je prikazan spodaj.

In [3]:
import sys
from PyQt5 import QtCore
from PyQt5 import QtGui
from PyQt5 import QtWidgets

class GlavnOkno(QtWidgets.QMainWindow):
    """ Glavno okno podeduje `QtWidgets.QMainWindow`
    """

    def __init__(self):
        """ Konstruktor GlavnoOkno objekta
        """
        QtWidgets.QMainWindow.__init__(self) # konstruktor starša
        self.setWindowTitle('Naslovno okno programa')
 
        # status bar
        self.moj_status_bar = QtWidgets.QStatusBar() # novi widget
        self.moj_status_bar.showMessage('Ta tekst po 10s izgine', 10000)
        self.setStatusBar(self.moj_status_bar) # tukaj se self.moj_status_bar priredi predpripravljenemu v QMainWindow

        # central widget
        self.gumb1 = QtWidgets.QPushButton('Gumb 1') # Gumb z napisom 'Gumb 1'
        self.gumb1.pressed.connect(self.akcija_pri_pritisku_gumba1) # povežemo pritisk s funkcijo za akcijo
        self.setCentralWidget(self.gumb1) # dodamo gumb v `CentralWidget`
        
        # menuji
        self.moj_izhod_akcija = QtWidgets.QAction('&Izhod', 
                                             self, shortcut=QtGui.QKeySequence.Close,
                                             statusTip="Izhod iz programa",
                                             triggered=self.close)
        self.moj_datoteka_menu = self.menuBar().addMenu('&Datoteka') # menu 'Datoteka'
        self.moj_datoteka_menu.addAction(self.moj_izhod_akcija) # povezava do zgoraj definirane akcije `self.moj_izhod_akcija`
        
    def akcija_pri_pritisku_gumba1(self):
        QtWidgets.QMessageBox.about(self,
                                'Naslov :)',
                                'Tole se sproži pri pritisku gumba.')

Program potem poženemo znotraj stavka try:

In [4]:
try:
    app = QtWidgets.QApplication(sys.argv)
    mainWindow = GlavnOkno()
    mainWindow.show()
    app.exec_()
    sys.exit(0)
except SystemExit:
    print('Zapiram okno.')
except:
    print(sys.exc_info())
Zapiram okno.

Za naprednejši zgled glejte datoteki:

Nekaj komentarjev na uporabniski_vmesnik.py:

  1. Poglejte prepis dogodka mouseDoubleClickEvent in prepišite podedovan dogodek keyPressEvent, ki naj ob pritisku katerekoli tipke zapre program (če se nahajate v TextEdit polju, potem seveda pritisk tipke izpiše vrednost te tipke).
  2. Dodajte še kakšen Widget s seznama.
  3. Spremenite program, da se bo vedno izrisovala funkcija sinus, v vpisno polje function_text pa boste zapisali število diskretnih točk (sedaj je točk 100). Povežite polje z ustreznimi funkcijami.
  4. Uredite lovljenje napak pri zgornji spremembi.

Nekaj vprašanj za razmislek!

  1. V PyCharm-u pripravite modul, ki bo imel dve funkciji:
    • za množenje matrike in vektorja,
    • za množenje dveh matrik.
  2. Za modul zgoraj pripravite skripto za testiranje (uporabite numpy.testing).
  3. V uporabniski_vmesnik_simple.py inicializijski metodi __init__ zakomentirajte vse klice na metode self.init ... razen na metodo: self.init_status_bar(). Poženite program v navadnem načinu. Nastavite break točko na self.setGeometry(50, 50, 600, 400) in poženite program v debug načinu.
  4. Nadaljujte prejšnjo točko in poiščite bližnjico za pomikanje po vrsticah:

    • s preskokom vrstice,
    • z vstopom v vrstico.

      Vstopite v init_status_bar(self) in se ustavite pri vrstici self.setStatusBar(self.status_bar). Odprite konzolo (console) in prek ukazne vrstice spremenite vrednost self.status_bar.showMessage().

  5. Odkomentirajte prej (zgoraj) zakomentirane vrstice. Dodajte tretji gumb, ki naj program zapre.
  6. Dodajte še kakšen Widget s seznama.