pyUnit
Testing
Python
Børre Stenseth

Testing av Pythonkode

Hva

Vi skal holde oss i "unit"-tradisjonen og se på PYUnit for testing av Pythonkode. PyUnit er en del av Pythondistribusjonen fra versjon 2.1(?).

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.

[1]
Referanser
  1. PyUnit sourceforge.net pyunit.sourceforge.net 14-03-2010