Behaviour Driven Development

BDD? TDD? Melyik milyen előnnyel szolgálhat számunkra? Milyen minőségjavulást hozhat, ha az egyik, vagy a másik módszerrel készítjük az implementációt? Melyik módszerrel tudjuk jobban az üzleti igényeket lefedni? Egyszerű példán keresztül bemutatva láthatjuk a különböző módszerekkel implementált alkalmazásokat és azok minőségbeli változásait.

Bevezetés

Dan North brit tesztelő újra és újra hasonló problémákba ütközött tréningjei során. Agile technikákkal kapcsolatban észrevette, hogy hallgatói és ő maga is munkája során ugyanazokkal a problémákkal szembesül. A hallgatók, akik unitteszteket írtak kódjaikhoz, általában a következő nehézségekkel találták szembe magukat:

  • Nem értették, hol kezdjék el a tesztelést, miből induljanak ki?
  • Melyek azok a részek, amit tesztelniük kell és melyeket nem?
  • Mennyit kell tesztelniük?
  • Nehéz megtalálni, megérteni, miért lett egy teszt sikertelen.

Akkoriban Test Driven Developmenttel (tesztvezérelt vagy tesztalapú fejlesztéssel) foglalkozott, ez szolgált a viselkedés alapú fejlesztés (BDD) alapjául. A BDD nem csak a TDD, hanem más, már meglévő fejlesztési és tesztelési metodikákat is felhasznál. Ilyenek pl. Domain-driven Development, Acceptance Test Driven Development, Definition of Done, User Stories , stb. A BDD lényege, hogy az adott szoftvertermék viselkedését veszi alapul, ezt használja fel tesztek, ill. az implementáció elkészítéséhez, ami azért jó, mert a viselkedés nagyon közel áll az üzleti követelményekhez, azaz jobban kiszolgálja a megrendelő elvárásait. Ez egyértelműen pozitív hatással van a minőségre.

Üzleti elvárások

Képzeljük el a következő problémát.

A megrendelőnk üzleti követelménye egy olyan program, ami kiszámítja és kiírja a képernyőre egy nem-negatív szám faktoriálisát. Hogyan valósítanánk ezt meg hagyományos szoftverfejlesztési modellben? Valószínűleg egy jelentős tervezési fázis után egyszer csak megérkezik a probléma egy fejlesztőhöz, aki megírja a programot, aminek a lelke maga a faktoriális függvény. A fejlesztőnk Pythonban implementált, és ezt a függvényt írta:

def factorial (number):
    if (number == 0) or (number == 1):
        return 1
    else: 
return number*factorial(number - 1)


Jól látszik, hogy a klasszikus, rekurzív algoritmust valósította meg. A tesztelők már alig várták, hogy megírják a teszteket a programhoz. A következő teszteket specifikálták a követelmények alapján:

Input        Elvárt eredmény
0 1
1 1
5 120
-2 Wrong INPUT!
3,2 Wrong INPUT!
asd Wrong INPUT!
 

Bizony, az utolsó 3 tesztesetnek nem felelt meg az implementáció, tesztelőink hibát találtak, ugyanis a függvény nem kezeli a hibás bemenetet. Ezek után fejlesztőnk a következőre javította a függvényt:

def factorial(number):
    if (number < 0):
        print("Wrong INPUT!")
        return -1
    if (number == 0) or (number == 1):
        return 1
    else:
        return number * factorial(number - 1)


A hibakezelést úgy oldotta meg, hogy ha egy nem nem-negatív input érkezik, akkor a függvénynek -1-et ad át. Így az utolsó 3 teszteset is lefut sikeresen.

Ugyanez TDD-ben

A Test Driven Developmentről manapság is könyveket írnak, ennek a cikknek nem feladata a TDD teljes tárgyalása, csak annyira említjük meg, amennyi kapcsolata van a BDD-vel. Nézzük, meg, hogy egy másik cég, aki TDD-ben fejleszt, hogyan valósítaná meg az előző feladatot. Szintén egy kezdeti tervezési fázis után nekiállnak a fejlesztésnek, azonban ebben a módszertanban a tesztek születnek meg először.

Tegyük fel, hogy teljesen ugyanazokat a teszteket specifikálják a tesztelők, mint az előbb, de még implementáció nincs hozzá. Ezek után a fejlesztők megírják az implementációt, de úgy, hogy szem előtt tartják a már megvalósított teszteket. Így születik meg pontosan ugyanaz a javított kód, mint az előző bekezdésben. Tehát a fejlesztők (beszélhetünk unit tesztről is, így a tesztelő és a fejlesztő személye egybe eshet) rá vannak kényszerítve, hogy megfeleljen az implementáció a már megírt teszteknek. Ezt a „tests first” megközelítést használják fel a BDD-ben is.

Ugyanez BDD-ben

Valósítsuk meg BDD-vel az előbbieket mi magunk! Az implementálást Pythonban készítjük el, a BDD-eszköz pedig legyen a lettuce (www.lettuce.it)! Az előbbi példa Gabriel Falcaotól származik, ő a lettuce megalkotója. A lettuce egy Python kiegészítés, ami lehetővé teszi a BDD-tesztek írását Pythonban. Először írjuk körül a viselkedését, ebből generálódnak a tesztesetek, majd folyamatosan készül el az implementáció is. Itt a legfőbb kapcsolat a TDD-vel, hogy a tesztek itt is megelőzik a megvalósítást.

Nézzük a gyakorlatban!

Először szerezzük be a lettuce-t! Az installáláshoz bővebben itt találunk segítséget: http://lettuce.it/intro/install.html#intro-install. Valamint érdemes megnézni a Quick Start Tutorial részt, ahol ugyanez a faktoriális probléma van részletezve.

Ha az installáláson túlvagyunk, hozzuk létre az alábbi könyvtárstruktúrát:

<tetszőleges elérési út>\factorial
     | tests
           | features
                - zero.feature
                - steps.py 


Hozzuk létre a zero.feature és a steps.py üres fájlokat. Egyikben írjuk le a viselkedést, a másikban az implementáció születik. Írjuk be a következőket a zero.feature fájlba:

Feature: Compute factorial
    In order to learn Lettuce
    As beginners
    We'll implement factorial
    Scenario: Factorial of 0
        Given I have the number 0
        When I compute its factorial
        Then I see the number 1


A feature fájl Gherkin nyelven íródott, ami lehetővé teszi, hogy definiáljuk az éppen tesztelt feature-t (a faktoriális számítása), valamint a teszteseteket (Scenario). Ezek szerint az első tesztünk nullának a faktoriálisát ellenőrzi. Meghatározza a bemenetet (Given) és hogy milyen esemény hatására (When) milyen elvárt eredményt szeretnénk látni (Then). Bővebben a Gherkinről itt olvashatsz: https://github.com/cucumber/cucumber/wiki/Gherkin

Most írjuk meg az implementációt pythonban (steps.py):

from lettuce import *
@step('I have the number (\d+)')
def have_the_number(step, number):
    world.number = int(number)
@step('I compute its factorial')
def compute_its_factorial(step):
    world.number = factorial(world.number)
@step('I see the number (\d+)')
def check_number(step, expected):
    expected = int(expected)
    assert world.number == expected, \
        "Got %d" % world.number
def factorial(number):
    return -1 


Jól látható, hogy a feature-fájlban található mondatokhoz tartozik egy-egy függvény, így lesznek végrehajthatóak a tesztjeink. Jelenleg a függvényünk -1-et ad vissza. Nyissunk egy parancssort, álljunk a tests könyvtárba, majd futtatáshoz egyszerűen írjuk be: lettuce

Ekkor a lettuce megtalálja a feature-fájlban leírt Feature részt, valamint az egyetlen tesztesetet, majd futtatja azt, ami hibára fut, hiszen 0 faktoriálisa nem -1. A lettuce az egyes mondatokra rákeres a python-fájlban, majd az azt követő utasításokat végrehajtja. Ezek a mondatok természetesen többször is felhasználhatóak a teszt során. Most javítsuk ki a függvényünket! Írjuk meg a rekurzív faktoriális függvényt! A steps.py-ban javítsuk át a függvényt erre:

def factorial(number):
    number = int(number)
    if (number == 0) or (number == 1):
        return 1
    else:
        return number*factorial(number-1)


Most futtassuk újra a lettuce-t! Azt látjuk, hogy egyetlen tesztünk zöldre fut. Most adjunk hozzá több Scenariót a feature fájlhoz! Azaz bővítsük ki a teszteseteket! Pl. ellenőrizzük 1, ill. 5 faktoriálisát is. Nézzenek ki így a feature fájl Scenariói:

    Scenario: Factorial of 0
        Given I have the number 0
        When I compute its factorial
        Then I see the number 1
    Scenario: Factorial of 1
        Given I have the number 1
        When I compute its factorial
        Then I see the number 1
          Scenario: Factorial of 5
        Given I have the number 5
        When I compute its factorial
        Then I see the number 120


Futtassunk ismét, majd örüljünk, hiszen mind a 3 tesztünk pass. Egészítsük ki még 3 tesztesettel a Scenariókat, adjuk még hozzá a feature fájlhoz:

    Scenario: Factorial of a minus number
        Given I got a strange input, for example: -2
        When I compute its factorial
        Then I see an error message
 
    Scenario: String input for factorial
        Given I got a strange input, for example: asd
        When I compute its factorial
        Then I see an error message
    Scenario: Factorial of a strange number
        Given I got a strange input, for example: 3.12
        When I compute its factorial
        Then I see an error message


Ha most futtatunk, akkor faild-re fut az utolsó 3 tesztünk, hiszen még az implementáció nem kezeli a hibás bemenetet. Sőt az „I got a strange input, for example:” mondathoz/tesztlépéshez még nem tartozik utasítás, ezért is reklamál a lettuce. Javítsuk ki steps.py-t a következőre:

from lettuce import *
@step('I have the number (\d+)')
def have_the_number(step, number):
    world.number = int(number)
 
@step(u'I got a strange input, for example: (.*\D+)')
def have_the_number(step, number):
    world.number = -1
 
@step('I compute its factorial')
def compute_its_factorial(step):
    world.number = factorial(world.number)
 
@step('I see the number (\d+)')
def check_number(step, expected):
    expected = int(expected)
    assert world.number == expected, "Got %d" % world.number
 
@step('I see an error message')
def error_message(step):
    assert world.number == -1, 'Wrong INPUT!'

def factorial(number):
    if (number<0):
        print("Wrong INPUT!")
        return -1
    if (number==0) or (number==1):
        return 1
    else:
        return number*factorial(number-1)


Jól látható, hogy implementáltuk a hibás bemenethez tartozó tesztlépést, ami nem tesz mást, mint -1-gyel hívja meg függvényünket, ami fel van készítve, hogy hiba üzenetet jelentessen meg, valamint speciálisan -1-et ad vissza. Ha most futtatunk, láthatjuk, az összes tesztesetünk sikeresen lefut.

Összefoglalás

A BDD legnagyobb előnye az érthetőség. Ha ugyanis a tesztünk hibára fut, jól látható, hogy melyik lépés volt sikertelen. Ezt olyan szöveges formában kapjuk meg, amit egy nem szakmai ember is megért. Sőt lehetőség van a Gherkin kifejezések testreszabására is. Így akár magyarul is definiálhatjuk a mondatokat. Ezek a mondatok nem csak azt mutatják meg, hogy hol futott hibára a teszt, hanem azt is, hogy éppen mit tesztelünk és az milyen üzleti követelménnyel van kapcsolatban, növelve ezzel a megértést és a tesztelési lefedettséget. Ma már minden programozási nyelvhez találunk BDD eszközt. A php-hez behat, a Javához JBehave, a c#-hoz SpecFlow vagy pl. a Ruby-hez a Cucumber áll a rendelkezésünkre.

Szerző:
Tóth Árpád

<< Vissza