Introduksjon til testdreven programmering med python
Hva er testdreven utvikling?
Testdreven utvikling (TDD) er en arbeidsmetodikk som baserer seg på at man skriver testen først, så selve koden. Dette betyr at i utgangspunktet vil alle tester feile første gangen. Jeg skal vise et eksempel på dette straks.
Programming without tests is like mowing the lawn in the dark. You never know when your done or how bad you messed up.
Om du vil lese mer om TDD så er Wikipedia en god start: http://en.wikipedia.org/wiki/Test-driven_development
Rammeverk for testing
Et av hovedmålene med testdreven utvikling er at det skal være enkelt å skrive tester, og at testingen skal foregå automatisk. For å hjelpe oss med dette finnes det forskjellige rammeverk vi kan bruke.
Noen testrammeverk som kan brukes med Python:
- doctest
- unittest
- nose
- pytest
I denne posten tar jeg utgangspunkt i unittest. Dette er python sin variant av xUnit rammeverket som man finner i mange andre programmeringsspråk.
Dette rammeverket er inkludert som en av standardmodulene i python, så vi trenger ikke å installere noe ekstra for å komme igang.
Merk at alle testmetoder vi skal bruke i unittest må begynne med navnet "test_".
Vår første test
I denne første testen jobber jeg med to filer
- koden som skal testes, numpyrunner.py
- selve testene, numpyrunner_test.py
Til å begynne med ser filene slik ut:
numpyrunner_test.py
from datetime import datetime
import unittest
from numpyrunner import Numpyrunner
class NumpyTestClass(unittest.TestCase):
def setUp(self):
self.numper = Numpyrunner()
def test_numpyrunner_add_method_return_correct_result(self):
result = self.numper.add(2, 2)
self.assertEqual(4, result)
def main():
unittest.main()
#We can run our test by issuing: python numpyrunner_test.py
if __name__ == "__main__":
main()
numpyrunner.py
import numpy as np
class Numpyrunner(object):
def statement():
print "Hello world"
Det vi nå har gjort er at vi har opprettet en test for å verifisere at dersom vi legger sammen tallene 2 og 2 så får vi tallet 4.
- Først setter vi opp testen vår ved å opprette en instans av klassen Numpyrunner
- Deretter utfører vi testen ved å kalle metoden add(x, y) på klassen, og sammenligner resultatet med en hardkodet verdi (assertEqual(4, result))
La oss prøve å kjøre testen vår:
$ python numpyrunner_test.py
E
======================================================================
ERROR: test_numpyrunner_add_method_return_correct_result (__main__.NumpyTestClass)
----------------------------------------------------------------------
Traceback (most recent call last):
File "numpyrunner_test.py", line 13, in test_numpyrunner_add_method_return_correct_result
result = self.numper.add(2, 2)
AttributeError: 'Numpyrunner' object has no attribute 'add'
----------------------------------------------------------------------
Ran 1 test in 0.000s
FAILED (errors=1)
Som vi ser så kjørte testen vår, men den feilet. Vi får også en pekepinn på hvorfor den feilet.
Bonuspoeng om du allerede har skjønt hvorfor den feilet!
Målet med testdreven utvikling er å lage et program som får alle testene til å passere. Med andre ord så skriver vi tester før vi skriver selve koden.
Testen i eksempelet over feiler siden vi ennå ikke har implementert add(x, y) metoden i koden vår. Så at denne testen feilet var forventet.
La oss fikse koden vår slik at testen vår fungerer:
numpyrunner.py
import numpy as np
class Numpyrunner(object):
def statement():
print "Hello world"
def add(self, x, y):
number_types = (int, long, float, complex)
if isinstance(x, number_types) and isinstance(y, number_types):
return x+y
else:
raise ValueError
og så kjører vi testen på ny:
$ python numpyrunner_test.py
.
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
Vi har nå kode som gjør det vi forventer at den skal gjøre, og vi har en test som kan verifisere dette.
La oss utvide testene våre
Som du ser i eksempelet over så benytter vi oss av en metode i unittest som heter assertEqual(x, y) som sammenligner verdiene i x og y. Dersom de er like returneres true, og testen passerer. Om de ikke er like returneres false, og testen feiler.
Som vi skal se snart så kan numpyrunner_test.py inneholde flere tester og noen kan feile, mens andre går igjennom.
I tillegg til assertEqual(x, y) finnes det en hel rekke andre asserts vi kan bruke i testene våre. Du finner en liste her: https://docs.python.org/2/library/unittest.html#assert-methods
numpyrunner_test.py
from datetime import datetime
import unittest
from numpyrunner import Numpyrunner
class NumpyTestClass(unittest.TestCase):
def setUp(self):
self.numper = Numpyrunner()
def test_numpyrunner_add_method_return_correct_result(self):
result = self.numper.add(2, 2)
self.assertEqual(4, result)
def test_numpyrunner_add_method_return_error_if_argument_not_numbers(self):
self.assertRaises(ValueError, self.numper.add, 'two', 'three')
def test_numpyrunner_pythonsum_return_correct_result(self):
result = self.numper.pythonsum(4)
self.assertEqual([0, 2, 8, 18], result)
def test_numpyrunner_numpysum_return_correct_result(self):
result = self.numper.numpysum(4)
self.assertEqual([0, 2, 8, 18], result)
def test_numpyrunner_sum_equal(self):
res1 = self.numper.pythonsum(100)
res2 = self.numper.numpysum(100)
self.assertEqual(res1, res2)
def test_numpyrunner_time_differ(self):
start = datetime.now()
delta1 = datetime.now() - start
start = datetime.now()
delta2 = datetime.now() - start
self.assertGreater(delta1, delta2)
def main():
unittest.main()
#We can run our test by issuing: python numpyrunner_test.py
#If we have nosetests available we can issue: nosetests numpyrunner_test.py -sv
if __name__ == "__main__":
main()
numpyrunner.py
import numpy as np
class Numpyrunner(object):
def statement():
print "Hello world"
def add(self, x, y):
number_types = (int, long, float, complex)
if isinstance(x, number_types) and isinstance(y, number_types):
return x+y
else:
raise ValueError
def pythonsum(self, n):
a = range(n)
b = range(n)
c = []
for i in range(len(a)):
a[i] = i ** 2
b[i] = i ** 2
c.append(a[i] + b[i])
return c
def numpysum(self, n):
a = np.arange(n) ** 2
b = np.arange(n) ** 2
c = a + b
return c.tolist()
Vi har nå lagt til flere tester, og implementert de metodene i koden som skal til for at testene skal fungere.
La oss prøve å kjøre testene våre engang til:
$ python numpyrunner_test.py
......
----------------------------------------------------------------------
Ran 6 tests in 0.001s
OK
Vi har nå 6 tester, og alle testene passerer. Jeg går ikke noe nærmere inn i selve koden her, men dette er da altså den samme koden som jeg brukte i Komme i gang med NumPy og SciPy posten.
Oppsummering
Dette var en rask introduksjon til hvordan man kommer igang med TDD i python. Om en ikke er vant til TDD så kan det først virke litt bakvendt å skrive tester før en faktisk har noe kode som kan testes. Men dersom en prøver litt oppdager man at dette er en ganske interessant tilnærming til programvareutvikling.
Om du kjøper påstanden om at kode bør testes, vil en testdreven tilnærming sørge for at du på ethvert tidspunkt faktisk har testbar kode.
Om du velger å skrive testene i etterkant vil man fort kunne ende med å skrive tester som man vet passerer gitt den underliggende koden, man tilpasser testene til koden fremfor å tilpasse koden til testene.
Om du er interessert i å lære mer om testdreven utvikling finnes det mye litteratur og ressurser som tar for seg emnet generelt.
Om du har noen spesielle bøker eller nettadresser om temaet som du kan anbefale så legg gjerne igjen en tilbakemelding i kommentarfeltet.