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.