Az oldal írásának pillanatában általánosan nose-t használunk unit teszteléshez. Ez az oldal a unit tesztelést taglalja részletesen.
A CIRCLE projekthez használunk még selenium-ot end-to-end teszteléshez. Erről bővebb információ a Selenium tesztelés egy DevEnvről wikin olvasható.
Tesztek futtatása
A teszteket legegyszerűbben a nosetests [tesztfájlok vagy teszteket tartalmazó könyvtárak]
paranccsal futtathatod. Pl.: nosetests tests.py
Bővebb részletek itt: http://nose.readthedocs.org/en/latest/usage.html
Django alatt
A Django projekt tesztjeit a ./manage.py test [<modul neve>] --settings="circle.settings.test"
paranccsal futtathatod.
- Összes teszt futtatása:
./manage.py test --settings="circle.settings.test"
- Csak a school app tesztjei:
./manage.py test school --settings="circle.settings.test"
- Konkrét modulhoz (pl.
school.models
) kapcsolódó tesztek:./manage.py test school.tests.test_models --settings="circle.settings.test"
- Konkré test case futtatása:
./manage.py test school.tests.test_models:PersonTestCase --settings="circle.settings.test"
- Konkrét teszt futtatása:
./manage.py test school.tests.test_models:PersonTestCase.test_some_feature_of_person --settings="circle.settings.test"
- Az általános formátum :
<app név>.tests.test_<modul név>:<test case név>.<teszt név>
coverage
A kód tesztek általi lefedettségét a nose
beépített coverage.py
moduljával ellenőrizheted.
Tesztek futtatása coverage-dzsel: nosetests [tesztelendők] --with-coverage
Django alatt: ./manage.py test [<modul>] --with-coverage --settings="circle.settings.test"
pragma: no cover
A kódban kihagyhatunk sorokat, ágakat a coverage vizsgálatból, ha pragma: no cover
megjegyzéssel jelöljük meg őket:
if this_value_is_almost_always_true:
do_usual_stuff()
else: # pragma: no cover
launch_nuclear_missiles() # ezt inkább nem akarom tesztelni
vagy
try:
this_function_never_throws()
except: # pragma: no cover
logger.error('The world has stopped.') # ez a sor úgysem hívódik meg soha, de legalábbis nem tudom tesztelni
Megjegyzem, nagyon ritkán van erre szükség. Általában inkább code smell, ami rossz tervezésre/megvalósításra, nem tesztelhető kódra utal.
További infók
Tesztek készítése
Mikor?
Ha írsz valamilyen új kódot, még a kód megírása előtt – de legkésőbb rögtön utána – készíts hozzá teszteket!
Ha javítasz egy bugot, előbb készíts tesztesetet, ami reprodukálja, csak ezután kezdj neki a munkának! Ha tényleg kijavítottad a hibát, a teszt is sikeresen le fog futni.
Hova?
A unittest-eket az adott egységet (unit) tartalmazó modulhoz tartozó tesztmodulba szoktuk elhelyezni. A tesztmodul megegyezés szerint a test_<modul név>
nevet viseli. Pl. my_module
modulhoz a test_my_module
tartozik.
Django alatt ez a tesztmodul megegyezés szerint a modult tartalmazó app tests
package-ében található, test_<modul név>
néven. Pl. a school.models.Person
osztályhoz kapcsolódó tesztek a school.tests.test_models
modulba kerülnek.
Mit?
Gondold át, mik az adott egység invariánsai? Mik azok a szélsőséges állapotok, argumentumok (függvény bemeneti paramétereinek lehetséges értékei), amik hibát okozhatnak? Ez segíthet az adott kód helyes megírásában is.
Ezután először írj olyan teszteket, amik az egység helyes működését tesztelik. Hozd létre az objektumot vagy hívd meg a függvényt úgy, ahogy szerinted helyes! Ellenőrizd, hogy a létrejött objektum állapota helyes-e, jó-e a függvény visszatérési értéke!
# a.py
class Connector:
connection_url = None
def connect(url):
connection_url = url
#test_a.py
from a import Connector
def test_Connector_connect():
connector = Connector()
url = "http://example.org"
connector.connect(url)
assert connector.connection_url == url
Ha ezzel megvagy, menj át hacker-módba, és próbáld meg megsérteni a feltételeket! Hozd létre az objektumot vagy hívd meg a függvényt hibás paraméterekkel! Ha sikerült, az azt jelenti, hogy hiányzik még pár ellenőrzés a kódodból, amiket pótolni kell.
# a.py
class Connector:
connection_url = None
def connect(url):
connection_url = url
# test_a.py
from a import Connector
def test_Connector_connect_with_invalid_url():
connector = Connector()
url = 42
connector.connect(url) # erre hibát kéne dobnia, hisz a 42 nem egy helyes URL, de a kód ezt MÉG nem ellenőrzi
Hogyan?
A tesztek neve megállapodás szerint test_
előtaggal kezdődik és jól leírja, hogy mit tesztelnek. Nem baj, ha hosszú, úgysem fogja kézzel leírni senki sem. Pl.:
def test_foo_with_value_greater_than_100():
# ...
!!! Egy teszt egyszerre csak egy dolgot vizsgál !!! Vagyis nincs több hívás része (lásd alább).
A tesztek általában a következő felépítést követik:
def test_foo():
# 1. előkészítés
# 2. hívás
# 3. ellenőrzés
# 4. takarítás
- Előkészítés (setup): Ebben a fázisban kell a teszthez szükséges adatokat létrehozni, erőforrásokat lefoglalni (pl.: kapcsolódni adatbázishoz, fájt megnyitni, szükséges entitásokat eltárolni az adatbázisban).
- Hívás: Ebben a fázisban történik magának a tesztelendő működésnek a végrehajtása. Ez lehet egy egyszerű függvényhívás az előzőleg előkészített adatok és erőforrások felhasználásával, de lehet akár hívások sorozata is. Fontos, hogy közben már szabad újabb előkészítési fázisba tartozó műveletet csinálni.
- Ellenőrzés (assertion): Ebben a fázisban ellenőrizzük a működés hatását, eredményét. A hagyományos unittest-eknél ez általában az érintett objektumok állapotának vizsgálatát jelenti (pl. "100 lett-e a foo.size?", "létrejött-e az entitás az adatbázisban?"). Viselkedést vizsgáló teszteknél (mock használatával) a hívott metódusokat, azok hívását, a hívások sorrendjét és paramétereit szoktuk vizsgálni.
- Takarítás (teardown): Ebben a fázisban végezzük el a felesleges adatok törlését, erőforrások elengedését, ha szükséges.
Példa:
# a.py
def first_two_elements(list)
return (list[0], list[1])
# test_a.py
from a import first_two_elements
def test_first_two_elements():
list = ['a', 'b', 'c'] # előkészítés
res = first_two_elements(list) # hívás
assert len(res) == 2 # ellenőrzés...
assert res[0] == list[0] # ...
assert res[1] == list[1] # ...
A tesztelési keretrendszerek általában biztosítanak módszert tesztek előkészítés és takarítás fázisainak kifaktorálására és megosztására a tesztek között. A nose
a @with_setup(<setup>, <teardown>)
dekorátort használja erre a célra.
# test_vm.py
from nose import with_setup
from vm import create_vm, delete_vm, get_ram_usage, get_virtual_cpu_count
def create_vms():
create_vm(name="alpha", cpu=2, ram=1024)
create_vm(name="beta", cpu=1, ram=1024)
create_vm(name="gamma", cpu=6, ram=4096)
def delete_vms():
delete_vm(name="alpha")
delete_vm(name="beta")
delete_vm(name="gamma")
@with_setup(create_vms, delete_vms)
def test_get_ram_usage():
assert get_ram_usage() == (1024+1024+4096)
@with_setup(create_vms, delete_vms)
def test_get_virtual_cpu_count():
assert get_virtual_cpu_count() == (2+1+6)
Használhatóak továbbá a Python beépített tesztelési lehetőségei is. A unittest.TestCase
egyszerűbb megoldást biztosít adatok megosztására az előkészítés, az egyes tesztek és a takarítás között.
from unittest import TestCase
from auth import login, logout
class UserTestCase(TestCase):
def setUp(self):
self.user = login(username="tesztelek", password="correct horse battery staple")
def tearDown(self):
logout(self.user)
def test_home(self):
assert self.user.home == "/home/tesztelek"
kivételek
# a.py
def first_two_elements(list)
if len(list) < 2:
raise ValueError
else:
return (list[0], list[1])
# test_a.py
from nose.tools import raises
from a import first_two_elements
@raises(ValueError)
def test_first_two_elements_with_empty_list():
list = []
first_two_elements(list)
Viselkedés vizsgálata
A tesztek nem csak a hívások eredményeként előálló állapotot tudják vizsgálni, hanem azt is, hogyan kommunikálnak a résztvevőkkel (milyen metódusokat, milyen sorrendben, milyen paraméterekkel hívnak az egyes objektumokon). Ezt mock objektumok segítségével tehetjük meg.
from mock import Mock
from world import self_destruct
def test_world_self_desturct():
mock_world = Mock()
mock_world.destroy = Mock(return_value=True)
self_desturct(mock_world)
assert mock_world.destroy.called
assert mock_world.destory.call_count == 1
Akit érdekel a mock, stub, dummy és társaik közti különbség, annak ajánlom elolvasásra: