Introduksjon til testdreven programmering med python

Sat 29 March 2014

I denne posten tenkte jeg å dele noen av mine test-for-dummies erfaringer. Dette er ikke en post om testdreven utvikling generelt, men en måte man kan komme igang med det på dersom en jobber 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.