Testing av Pythonkode
Pythondokumentasjonen er ganske klar og eksplisitt når det gjelder filosofien i PyUnit. Jeg gjentar ikke dette her, men konsentrerer meg om noen konkrete eksempler.
Eksempel 1
Utgangspunktet er følgende "kravspesifikasjon":
Vi ønsker å skrive en modul med en funksjon som konverterer et naturlig tall, gitt som text, til en sekvens av tallord.
Vi skal implementere konverteringen i en modul som heter numbers, som metoden convert. På grunnlag av dette skriver vi en minimalistisk testmodule:
""" testing function convert in module numbers """ # import the module we want to test import numbers # import the testunit import unittest class testconversion(unittest.TestCase): def testConvert(self): # test conversion self.assert_(numbers.convert('124')=='en to fire') self.assert_(numbers.convert('12.4')==numbers.errormsg) self.assert_(numbers.convert('jensen')==numbers.errormsg) self.failUnless(numbers.convert('-13')==numbers.errormsg,'negativ') def makeTestSuite(): suite=unittest.TestSuite() suite.addTest(testconversion('testConvert')) return suite suite = makeTestSuite() unittest.TextTestRunner(verbosity=2).run(suite)
Vi starter med følgende forsøk på å skrive selve modulen:
""" Converting a natural number as string to numberwords s=convert(n) """ errormsg='ikke et tall' def convert(n): return errormsg
Hvis vi kjører (run) testmodulen får vi følgende resultat:
testConvert (__main__.testconversion) ... FAIL ====================================================================== FAIL: testConvert (__main__.testconversion) ---------------------------------------------------------------------- Traceback (most recent call last): File "F:\html\ml\pytest\testnumbers.py", line 10, in testConvert self.assert_(numbers0.convert('124')=='en to fire') AssertionError ---------------------------------------------------------------------- Ran 1 test in 0.016s FAILED (failures=1)
Vi skriver om modulen vår:
# -*- coding: cp1252 -*- """ Converting a natural number as string to numberwords s=convert(n) """ errormsg='ikke et tall' wds=['null','en','to','tre','fire','fem','seks','sju','åtte','ni'] def convert(n): if n.isdigit(): res='' for c in n: res+=wds[int(c)]+' ' return res[:-1] else: return errormsg
Vi tester igjen og får følgende testresultat:
testConvert (__main__.testconversion) ... ok ---------------------------------------------------------------------- Ran 1 test in 0.000s OK
Eksempel 2
Utgangspunktet er følgende "kravspesifikasjon":
Vi ønsker å skrive en modul som leser en fil fra filsystemet eller fra nettet, fjerner alle tagger med innhold og lager en oversikt over hvor mange ganger hver bokstav, a..å, forekommer i den rensede teksten. Vi skiller ikke mellom store (versaler) og små (minuskler) bokstaver. Resultatet skal rapporteres som en dictionary med bokstaven som nøkkel og antall forekomster som verdi.
Det første jeg gjør er å legge en plan for hvordan jeg ønsker å lage implementasjonen. Jeg kan skrive dette som en sammenhengende kodeblokk i modulen, eller jeg kan dele det opp i funksjoner. Jeg velger det siste, kalle modulen charstat, og lager følgende skjelett:
# -*- coding: cp1252 -*- """ loading from anywhere, remove tags and count letters, produce result as a dictionary """ import urllib errormsg='feil' #---------------------------------------- # load data from anywhere def loadData(address): return (errormsg) #---------------------------------------- # clean taggedcontent def cleanData(T): return '<>' #---------------------------------------- # count characters and store in a dictionary def countData(T): return {} def makeStatistics(address): t=loadData(address) if t!=errormsg: t=cleanData(t) resDict=countData(t)
Vi kunne planlagt etter mange andre strukturer, f.eks. en klasse eller en samlet metode eller rett og slett flat kode i modulen. Jeg har valgt den inndelingen jeg har for å kontrollere stegene i funksjonaliteten bedre. Jeg kommer til å teste koden deretter.
Det neste jeg gjør er å lage en modul som skal teste koden min.
""" testing charstat """ #import the module we want to test import charstat #import the tester import unittest class testcharcounter(unittest.TestCase): def setUp(self): # set up a list of different address we want to handle self.addresses=["c:\\articles\\ml\\pytest\\wordfile1.txt", "wordfile1.txt", "http://www.ia.hiof.no/~borres/ml/index.shtml", "http://www.ia.hiof.no/~borres/ml/python/frej1.txt", "wordfile1.txt"] def testLoadData(self): # test loading for ad in self.addresses: self.failUnless(charstat.loadData(ad)!=charstat.errormsg) def testClean(self): # test cleaning for ad in self.addresses: T=charstat.loadData(ad) T=charstat.cleanData(T) self.failUnless(T.find('<')==-1) self.failUnless(T.find('>')==-1) def testCount(self): # test counting T=charstat.loadData(self.addresses[0]) T=charstat.cleanData(T) count=charstat.countData(T) self.failUnless(count['e']==5) def makeTestSuite(): suite=unittest.TestSuite() # comment/uncomment as we go suite.addTest(testcharcounter('testLoadData')) suite.addTest(testcharcounter('testClean')) suite.addTest(testcharcounter('testCount')) return suite suite = makeTestSuite() unittest.TextTestRunner(verbosity=2).run(suite)
Hvis vi kjører (run) denne modulen får vi følgende resultat:
testLoadData (__main__.testcharcounter) ... FAIL ====================================================================== FAIL: testLoadData (__main__.testcharcounter) ---------------------------------------------------------------------- Traceback (most recent call last): File "F:\html\ml\pytest\testcharstat.py", line 22, in testLoadData self.failUnless(charstat0.loadData(ad)!=charstat0.errormsg) AssertionError ---------------------------------------------------------------------- Ran 1 test in 0.016s FAILED (failures=1)
det gikk dårlig. Dette er ikke overraskende siden vi ikke har implementert loadData. Vi vender tilbake til modulen vår og forsøker oss med en implementasjon. Etter litt prøving og feiling sitter vi kanskje med følgende:
# -*- coding: cp1252 -*- """ loading from anywhere, remove tags and count letters, produce a dictionary """ import urllib errormsg='feil' #---------------------------------------- # load data from anywhere def loadData(address): try: # as an url, possibly with scheme file: f=urllib.urlopen(address) res=f.read() f.close() return (res) except: # try as a strait filename (absolute or relative) try: file=open(address,'r') res=file.read() file.close() return (res) except: return (errormsg) #---------------------------------------- # clean taggedcontent def cleanData(T): tag_is_on=False res='' for ch in T: if ch =='<': tag_is_on=True elif ch =='>': tag_is_on=False elif not tag_is_on: res=res+ch return res #---------------------------------------- # count characters and store in a dictionary def countData(T): countables='abcdefghijklmnopqrstuvwxyz���' res={} for ch in countables: res[ch]=0 T=T.lower() for ch in T: if countables.find(ch)!= -1: res[ch]+=1 return res def makeStatistics(address): t=loadData(address) if t!=errormsg: t=cleanData(t) resDict=countData(t)
Vi tester igjen, og resultatet av testen blir:
testLoadData (__main__.testcharcounter) ... ok testClean (__main__.testcharcounter) ... ok testCount (__main__.testcharcounter) ... ok ---------------------------------------------------------------------- Ran 3 tests in 0.563s OK
Merk at vi kan velge hvilke tester vi ønsker å kjøre ved å bestemme hvordan vi bygger opp testsuite:
def makeTestSuite(): suite=unittest.TestSuite() suite.addTest(testcharcounter('testLoadData')) suite.addTest(testcharcounter('testClean')) suite.addTest(testcharcounter('testCount')) suite.addTest(testcharcounter('testGetStatistics')) return suite
Det kan stilles mange spørsmål til de testene som er laget.
- Som det framgår har jeg testet flere filer som jeg når på forskjellig måte. Dersom en fil ikke eksisterer skal programmet kunne handtere dette. Jeg har ikke eksplisitt testet dette.
- Jeg har testet tellingen mot et kjent resultat i en fil: self.assert_(dict['e'] == 5). Det kan diskuteres om dette er bra nok.
- Videre kan en diskutere detaljer i alle testfunksjonene og testene kan lett lage mer omfattende.
Det kan være en bra øvelse og bygge ut testbatteriet. Kopier filene charstat.py og testcharstat.py og forsøk deg fram.