Ohjelmoinnin peruskurssi Y2, kurssimateriaali

3.2. Pythonin testausvälineet

«  3.1. Ohjelmistojen testausta   ::   Etusivulle   ::   3.3. Tehtävä: Funktion weekdays testaaminen (130 p)  »

3.2. Pythonin testausvälineet

Pythonin moduli unittest

Python Standard Libraryn mukana tulee valmis yksikkötestausmoduli unittest, jonka dokumentaatioon kannattaa tutustua. Tulet käyttämään sitä myöhemmissä harjoituksissa. Myös palauttamasi ohjelmointitehtävien vastaukset tarkistetaan käyttäen unittestia.

Unittest on oliopohjainen ja sen ytimessä on luokka TestCase. Luokan metodin setUp avulla pystytetään tarvittaessa testipenkki (tästä käytetään nimeä fixture) ja metodilla tearDown se puretaan. Varsinaiset testit toteutetaan metodeina, joiden nimi alkaa merkkijonolla test_. Testimetodit sisältävät kutsuja assert -metodeihin, joiden avulla tarkistetaan, toimiiko testattava koodi oikein. Testi suoritetaan metodilla run. Assert -metodeja on tarjolla paljon, mm.

Metodi Tarkistus Negaatio
assertEqual Ovatko kaksi oliota samanlaiset assertNotEqual
assertTrue Tuottaako lauseke totuusarvon True tai vastaavan assertFalse
assertIs Ovatko kaksi oliota identiteetiltään samat assertIsNot
assertIsNone Onko arvo sama kuin None assertIsNotNone
assertIn Kuuluuko arvo joukkoon assertNotIn
assertIsInstance Onko arvo luokan instanssi assertNotIsInstance
assertRaises Tuottaako kutsu poikkeuksen  
assertGreater Onko arvo suurempi kuin assertLessEqual
assertLess Onko arvo pienempi kuin assertGreaterEqual

Kuten yleensäkin yksikkötestauksessa, testattava kohde on yleensä jokin funktio, luokka tai metodi. Testausta varten teemme luokasta TestCase uuden alaluokan ja kirjoitamme sille joukon testimetodeja. Tarvittaessa vielä määritämme setUp ja tearDown -metodit testipenkin pystyttämiseen.

Esimerkki TestCasen käytöstä

Haluamme testata funktiota find_subseq, jolle annetaan parametrina kaksi sekvenssiä seq ja sub ja joka palauttaa pienimmän indeksin, josta alkaen seq sisältää alisekvenssinään subin. Esimerkiksi kutsun find_subseq([3, 2, 10, 1, 3], [2, 10]) pitäisi palauttaa arvonaan 1. Tämä vastaa merkkijonojen metodia str.find. Meillä on seuraava koodi (find_subseq.py):

def find_subseq(seq, sub):
    '''
    Return the first index in seq, where seq contains sub as a subsequence.
    Return None, if sub is not a subsequence of seq.
    '''
    for i in range(len(seq)):
        found = True
        for j in range(len(sub)):
            if seq[i + j] != sub[j]:
                found = False
                break
        if found:
            return i
    return None

Millä syötteillä tätä pitäisi testata? Otetaanko black-box vai white-box -lähestymistapa? Aletaan vaikka white-boxilla ja pyritään kattamaan kaikki lauseet. Määritetään testiluokka TestFindSubseq (tiedostossa test_find_subseq.py). Tehdään testi, jossa sub esiintyy seq:issä ja tarkistetaan, että palautettu indeksi on oikea. Tehdään toinen testi, jossa sub esiintyy kaksi kertaa ja tarkistetaan, että taas saadaan oikea indeksi. Tehdään kolmas testi, jossa sub ei esiinny seq:issä ja tarkistetaan, että tuloksena on None.

import unittest

from find_subseq import find_subseq


class TestFindSubseq(unittest.TestCase):

    def test_found(self):
        self.assertEqual(find_subseq([3, 4, 1, 5], [4, 1]), 1)

    def test_found_first(self):
        self.assertEqual(find_subseq([3, 4, 1, 5, 5, 1], [1]), 2)

    def test_not_found(self):
        self.assertIsNone(find_subseq([3, 4, 1, 5], [2, 10]))


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

Testiohjelman lopussa on kutsu unittest.main(). Tämä on suoraviivainen tapa suorittaa saman modulin sisältämät testit.

Suoritetaan testit:

> python3 test_find_subseq.py
..
----------------------------------------------------------------------
Ran 3 tests in 0.000s

OK

Molemmat testit onnistuivat ja testit kävivät kaiken koodin läpi. Hienoa!

Vai oliko sittenkään? Pitäisikö kuitenkin miettiä tarkemmin, mitä tapauksia syötteissä voi esiintyä. On hyvä ajatella myös ääriarvoja, esimerkiksi tyhjiä sekvenssejä. Toimiiko find_subseq oikein, jos etsimme tyhjää alisekvenssiä? Määritelmällisesti voidaan ajatella, että tyhjä sekvenssi on minkä tahansa sekvenssin seq alisekvenssi ja ensimmäinen esiintymä on indeksissä nolla. Entä jos seq on tyhjä ja sub ei? Tuloksena pitäisi varmaan olla None. Lisätään testit näille:

def test_empty_sub(self):
    self.assertEqual(find_subseq([1, 2, 3], []), 0)

def test_empty_seq(self):
    self.assertIsNone(find_subseq([], [1, 2, 3]))

ja suoritetaan:

> python3 test_find_subseq.py
....
----------------------------------------------------------------------
Ran 5 tests in 0.000s

OK

Oliko tässä kaikki? Toimiiko find_subseq kaikissa tapauksissa oikein? Palataan white-boxin pariin ja tutkaillaan vielä koodia. Missä kohtaa koodi voisi tuottaa poikkeuksen jollain syötteillä? Sekvenssin alkioiden indeksointi on operaatio, joka voi mennä pieleen. Meillä on yksi rivi, jossa sekvenssejä indeksoidaan:

if seq[i + j] != sub[j]:

Tässä 0 <= j <= len(sub) - 1, joten sub[j] ei voi mennä pieleen. Sen sijaan 0 <= i + j <= len(seq) + len(sub) - 2.

Hetkinen! Tämähän voi olla suurempi tai yhtäsuuri kuin len(seq). Kyseinen vertailu suoritetaan tilanteessa, jossa len(sub) > 1 ja seq sisältää lopussaan sekvenssin sub alun. Laaditaan tällainen testi:

def test_long_sub(self):
    self.assertIsNone(find_subseq([1, 2, 3], [3, 4]))

ja suoritetaan:

> python3 test_find_subseq.py
....E.
======================================================================
ERROR: test_long_sub (__main__.TestFindSubseq)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "test_find_subseq.py", line 24, in test_long_sub
    self.assertIsNone(find_subseq([1, 2, 3], [3, 4]))
  File "/Users/enu/csgit/y2-course-material/software-testing/code/find_subseq.py", line 9, in find_subseq
    if seq[i + j] != sub[j]:
IndexError: list index out of range

----------------------------------------------------------------------
Ran 6 tests in 0.000s

FAILED (errors=1)

Haa! Löysimme virheen!

Oikeastaan voimme tämän pohjalta korjatakin tuon virheen. Millä sekvenssin seq indeksivälillä alisekvenssin sub alkukohta voi olla? Jotta sub mahtuisi seqin sisään pitää alisekvenssin alkaa viimeistään indeksistä len(seq)-len(sub), eli 0 <= i <= len(seq) - len(sub). Esimerkiksi jos len(sub)=3, löytyy sub viimeistään alkaen kohdasta len(seq)-3. Muutetaan ensimmäinen for-silmukka ja saadaan:

def find_subseq(seq, sub):
    '''
    Return the first index in seq, where seq contains sub as a subsequence.
    Return None, if sub is not a subsequence of seq.
    '''
    for i in range(len(seq) - len(sub) + 1):
        found = True
        for j in range(len(sub)):
            if seq[i + j] != sub[j]:
                found = False
                break
        if found:
            return i
    return None

If-lausessa on siten:

0 <= i + j <= (len(seq) - len(sub) + 1 - 1) + (len(sub) - 1) = len(seq) - 1 < len(seq)

joten indeksointi pysyy rajojen sisällä.

Muut luokat ja funktiot

Moduli unittest tarjoaa muita luokkia, joilla on oma roolinsa testien suorittamisessa. Näitä käytetään usein epäsuorasti kutsumalla unittest.main -funktiota, mutta toisinaan niitä voi joutua käyttämään myös suoraan ja määrittää niille omia alaluokkia. Luokat ovat:

  • TestSuite. Testeistä kootaan yhteen luokan TestSuite ilmentymiä, joita näitä voi käyttää kuten yksittäisiä TestCase-olioita eli niitä voi mm. ajaa run-metodilla.
  • TestLoader. Luo testiluokista ja moduleista TestSuiten. Tätä ei yleensä luoda itse, mutta jos halutaan esimerkiksi selektiivisesti poimia suoritettavat testit, voidaan se tehdä luokan TestLoader metodeilla discover, loadTestsFromTestCase, loadTestsFromModule, loadTestsFromName ja loadTestsFromNames. Jos halutaan käyttää omaa TestSuite-luokkaa, voi sen asettaa TestLoader-olion luonnin yhteydessä ominaisuuden suiteClass avulla.
  • TestRunner. Testien suorittaja, joka suorittaa TestSuiten. Tällä on konkreettinen alaluokka TextTestRunner, josta voi tehdä alaluokan tarvittaessa. Jos haluaa käyttää tulosluokkana omaa TestResult-luokan alaluokkaa, voi sen asettaa ominaisuudella resultclass TestRunner-olion luonnin yhteydessä.
  • TestResult. Testien tulokset kokoava olio. Tämän konkreettinen alaluokka on TextTestResult, josta voi tarvittaessa tehdä alaluokan ja asettaa sen TestRunner-olion käyttöön. Näin voi tehdä esimerkiksi, jos haluaa jotain erilaista raportointia testien suorituksen yhteydessä, kun määrittää uudestaan metodin startTest tai stopTest.
  • Suoritusmetodi unittest.main. Tämän avulla saa siis helpoiten testit suoritettua. Se ottaa koko joukon parametreja, joilla voi mm. asettaa käytettävä TestLoader- ja TestRunner-oliot sekä raportoinnin tason.

Pythonin moduli mock

Tällä saa tehtyä testauksessa käytettäviä mock-olioita, jotka simuloivat testissä tarvittavien, vielä toteuttamattomien ohjelman osien käyttäytymistä. Mock-olioiden käytöstä kerrotaan dokumentaation sivulla 26.6. unittest.mock — getting started.

Pythonin moduli doctest

Tämä moduli toteuttaa mielenkiintoisen testaustavan, jossa testit haluttuine vastauksineen poimitaan luokkien, metodien ja funktioiden dokumentaatiomerkkijonoista. Tarkempi kuvaus löytyy sivulta 26.3. doctest — Test interactive Python examples.

Palaute

«  3.1. Ohjelmistojen testausta   ::   Etusivulle   ::   3.3. Tehtävä: Funktion weekdays testaaminen (130 p)  »