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 sub
in. 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. ajaarun
-metodilla. - TestLoader. Luo testiluokista ja moduleista
TestSuite
n. Tätä ei yleensä luoda itse, mutta jos halutaan esimerkiksi selektiivisesti poimia suoritettavat testit, voidaan se tehdä luokanTestLoader
metodeilla discover, loadTestsFromTestCase, loadTestsFromModule, loadTestsFromName ja loadTestsFromNames. Jos halutaan käyttää omaaTestSuite
-luokkaa, voi sen asettaaTestLoader
-olion luonnin yhteydessä ominaisuudensuiteClass
avulla. TestRunner
. Testien suorittaja, joka suorittaaTestSuite
n. Tällä on konkreettinen alaluokka TextTestRunner, josta voi tehdä alaluokan tarvittaessa. Jos haluaa käyttää tulosluokkana omaaTestResult
-luokan alaluokkaa, voi sen asettaa ominaisuudellaresultclass
TestRunner
-olion luonnin yhteydessä.- TestResult. Testien tulokset kokoava olio. Tämän konkreettinen
alaluokka on
TextTestResult
, josta voi tarvittaessa tehdä alaluokan ja asettaa senTestRunner
-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
- jaTestRunner
-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.