Hogyan használják rosszul a BDD-t?
Bevezetés
A Behavior-Driven Development (röviden BDD) már egy 2000-es évek elejétől létező módszer. Még a mai napig is sokan használják, sőt vezetik be létező vagy új projekteken.
Sajnos azonban azt vettem észre, hogy sokan félreértik és hibásan használják. Az Interneten is rengeteg rossz példa terjed, sőt a különböző nagy nyelvi modellek is hibás megoldásokat hoznak (nyilván az előbbi példák alapján), ezekből is fogok többet is mutatni. Ezzel azonban nem segíti, hanem inkább hátráltatja a projekt előrehaladását.
Ebben a posztomban megpróbálom összegyűjteni, hogy hol lehet elrontani a BDD használatát, valamint milyen rossz gyakorlatokat látok a mai napig. Saját tapasztalatokat és erősen szubjektív elemeket is tartalmaz. Ez a korábbi Fejlesztőként mivel akadályozom a tesztelők munkáját? írásom folytatásaként is felfogható.
Röviden a BDD-ről
Nem célom a BDD részletes kifejtése, csak amennyi feltétlenül szükséges a megértéshez. Hivatalos definíció hiányában a kialakulásának céljait érdemes megérteni. Ez olyan módszer, megközelítés, melynek célja hogy a szoftver elvárt viselkedését példákon keresztül mindenki számára érthető, természetes nyelvi formában fogalmazza meg. Gyakran emlegetik a Three Amigos (magyarul: Három Barát) kulcsszereplőket is, ami arra utal, hogy az üzleti szereplők, fejlesztők és tesztelők közösen dolgoznak annak érdekében, hogy mindenki ugyanazt értse “kész alatt”. (Utalva itt az elfogadási kritériumokra - angolul: acceptance criteria.) Általánosan elterjedt még a Given-When-Then használata, mely szavak segítségével struktúrálni tudjuk az üzleti követelmények leírását. Elterjed eszköz a polyglott, azaz több programozási nyelven is használható Cucumber eszköz, valamint a nagyon egyszerű Gherkin nyelv.
Cucumber esetén a követelményeket ún. feature fájlban lehet leírni, mely forgatókönyveket, példákat (scenario) tartalmaz. Ezek lépésekből (step) állnak, az előbb említett Given-When-Then struktúrában. A természetes nyelven megfogalmazott lépést implementálni kell valamilyen programozási nyelven, ez az ún. step definition.
Példa projekt
Ehhez a poszthoz is készült egy példaprojekt, mely elérhető a GitHubon.
Klasszikus 3-rétegű Spring Boot alkalmazás. Webes felhasználói felülettel és REST API-val is rendelkezik. BDD eszközként a Cucumbert használom. A jellemzők a .feature
fájlokban vannak. Érdekessége, hogy a lépések implementálva vannak RestAssureddel a REST API tesztelésére, valamint Selenium WebDriverrel (page object tervezési mintával) a webes felület tesztelésére.
Az alkalmazás képzéseket tart nyilván (course
), melyeken van egy maximális
résztvevő szám (limit
). A képzésre jelentkezők jelentkezéseket adhatnak le
(applicant enroll
). Az újra jelentkezés nem ad hibát (idempotens).
Betelt tanfolyamra nem lehet jelentkezni.
Hiba: a BDD-t csak fejlesztők, vagy csak tesztelők használják
Az általam tapasztalt leggyakoribb hiba, hogy használják a BDD-t, Cucumbert, Gherkint, azonban vagy csak a fejlesztők, vagy csak a tesztelők. Ezzel pont a legalapvetőbb koncepciót sértik meg, hogy a BDD eszköze a Three Amigos kommunikációjának.
A fejlesztők, és a tesztautomatizálással foglalkozók sokkal jobban szeretnek a tapasztalatuknak, vagy érdeklődési körüknek megfelelő programozási nyelvet (pl. Python, Java, Kotlin, stb.) használni. Ha rájuk erőltetünk valami mást, mint pl a régen nagy divat XML nyelvet (lásd BPMN, BPEL, stb.), netán csak egy cégen belül használatos egyedi nyelvet, akkor (jogosan) csak ellenállásba fogunk ütközni. Lásd korábbi Miért ne fejlesszünk saját keretrendszert? című posztomat.
Gyakori jelenség, hogy egy “kiválasztott” alkotja meg a nyelvet, azért, hogy a “többiek” dolga könnyebb legyen. Sokszor a megalkotója nem is dolgozik benne, nem fogalmaz meg vele üzleti követelményeket, “másnak” csinálja, “másnak” segít ezzel. Személyes véleményem, különösen oktatóként, hogy egyáltalán nem kell másoknak egy leegyszerűsített nyelvet adni, pl. a Python nyelv elsajátítása mindenki számára megugorható, ráadásul másra is lehet használni. Attól, aki tesztautomatizálással akar foglalkozni, egy elterjedt nyelv megismerése elvárható. A mások segítése saját nyelvvel csak egy illúzió, igazi segítség az, ha egy általános célú, máshol is használható nyelv vagy eszköz elsajátítását támogatjuk.
Szerinted egy Java programozó melyiket írja szívesebben?
@Test
void announceCourse() {
courseService.announceCourse(new CourseAnnouncement("java-01", "Java", 3));
assertThat(courseService.findAll())
.extracting(CourseView::code)
.containsExactly("java-01");
}
Scenario: Announce a course
When I announce a course
Then it should be listed among the announced courses
A tesztelésre használható Java eszközök (JUnit, AssertJ, Mockito, stb.) rendkívül kiforrottak, egyszerűen használhatóak, igen olvasható és karbantartható kódot lehet velük alkotni. Unit, integrációs és E2E tesztek írására is különösen alkalmasak. Csak fejlesztők, tesztautomatizálók által használt BDD a legtöbb esetben felesleges.
Ráadásul a BDD a Test-Driven Development (TDD) finomításaként, kiegészítéseként jött létre. Tehát a koncepció, hogy először fogalmazzuk meg a követelményeket.
Ezzel kapcsolatban a Cucumber oldala is tartalmaz néhány tévhitet:
- Forgatókönyvek elkészítése a kód elkészítése után: sarkosan fogalmaz, ez egy jó tesztautomatizálási módszer, de ez nem BDD!
- Attól, hogy BDD eszközt (pl. Cucumber) használsz, még nem használsz BDD-t!
Hiba: nem üzleti követelmények rögzítése
Gyakori, hogy a BDD-t nem üzleti követelmények rögzítésére használják, és nem mindenki számára érthető állításokat fogalmaznak meg benne. Ilyen példákkal sajnos tele van az Internet. A leggyakoribb jelenség, hogy valamilyen technológia szerepel a leírásban.
Mutatok néhány elrettentő példát. Első példánkban a felhasználói felületre való utalások kerülnek a forgatókönyvekbe. Az üzlet nyelvét kell használni, deklaratív módon, és kerülni kell a felhasználói felületre való utalásokat!
Ezek elkerülésére csak kérdezzük meg magunktól, hogyha változik az implementáció, át kell-e írnunk a forgatókönyveket. Ha a válasz igen, akkor a forgatókönyv inkább imperatív jellegű, és érdemes átfogalmazni, hogy deklaratív legyen.
Scenario: Announce a course
When I fill in the code field with the value "java-01"
And I click the "Announce" button
Then the course appears in the table
Sajnos ennél még horrorabb megoldásokkal is találkozni. Kerülni kell a különböző technológiákat, CSS selectorokat, XPath kifejezéseket, stb.!
Scenario: Announce a course
When I fill in the "body > form:nth-child(2) > input[type=text]:nth-child(1)" field with the value 'java-01'
And I click the "/html/body/form[1]/button" button
Then the values of the "body > table > tbody > tr > td:nth-child(1)" selector contains the 'java-01' value
Ilyet láttam API hívásakor is. Kerülni kell a JSON, XML, XPath, JSON Path formátumok, kifejezések használatát is!
Scenario: Announce a course
When I send the following POST request to the "http://localhost:8080/api/courses" address:
"""
{
"code": "java-01",
"name": "Java",
"limit": 3
}
"""
Then I send a GET request to the "http://localhost:8080/api/courses" address, and the result of the "/name" JSON Path expression contains
the "java-01" value
Erre itt egy példa megvalósítás is: REST API Testing with Cucumber.
Az adatbázis műveletek sem maradhattak ki.
Scenario: Enroll in a course
Given I insert a course into "courses" table with "java-01" code
When John enrolls in the course
Then the "select count(enrollments) from courses left join course_enrollments on code=course_code group by code where code = java-01" query returns "2"
Kerülni kell az SQL utasításokat is!
Hiba: ubiquitous language mellőzése
A ubiquitous language egy DDD-ből átvett fogalom, és azt a célt fogalmazza meg, hogy egy közös üzleti nyelvet használjon mindenki. Ennek megsértése az előző eset, mikor technológiai fogalmakat használunk. De megsértése az is, hogyha ugyanarra az üzleti fogalomra teljesen más szavakat használunk.
A “tanfolyam meghirdetése” legyen announce course
, tehát hibásak a következők: create course
,
save course
, publish training
, stb.
A “jelentkező jelentkezik” legyen applicant enrolls
, akkor a applicant applies
,
candidate enrolls
, student registers
, create enrollment
hibás.
Gyakran látom, hogy a technológiákból jönnek a különböző megnevezések, mint pl. create
,
read
, update
, delete
, select
, insert
, post
, findAll
, save
. Ezek
tipikusan a perzisztenciára vonatkoznak, nem az üzleti elvárásokra.
Ugyanarra a fogalomra ugyanazt a megnevezést használjuk!
Hiba: teljesen felesleges részletek a forgatókönyvekben
Minden érték megadásra kerül a forgatókönyvben. Ez teljesen felesleges, érdemes csak a fontosabbakra koncentrálni. Nagyon jól használható az ún. “default values” tervezési minta, azaz a lényegtelen adatok helyére alapértelmezett, akár generált értékeket szúrunk be. Kerüljük az összes részlet megadását!
Scenario: Announce a course
When I announce a course with code "java-01", with name "Java", with limit 3.
Then it should be listed among the announced courses with code "java-01", with name "Java", with limit 3.
Hiba: csak atomi műveletekből való építkezés
Csak a lépések egy szűk készlete áll a rendelkezésünkre, és mindent ebből szeretnénk megvalósítani. Használjunk összevont lépéseket!
Scenario: Course is full
Given I announce a course
And John enroll in the course
And Jane enroll in the course
And Jack enroll in the course
When James enroll in the course
Then the number of the free seats should be 0
And it shouldn't be possible for attendees to enroll
Mennyivel egyszerűbb a következő:
Scenario: Course is full
Given I announce a course with a limit of 3
When 3 attendees enroll in the course
Then the number of the free seats should be 0
And it shouldn't be possible for attendees to enroll
Hiba: komplex, több esetet tartalmazó forgatókönyvek
Könnyen lehet olyan forgatókönyvet létrehozni, mely egyszerre több esetet fogalmaz meg, több ellenőrzés van benne. A komplex forgatókönyveket egy jellemző alatt több forgatókönyvre kell szétbontani!
Scenario: Announce a course
When I announce a course with a limit of 10
Then it should be listed among the announced courses
and it should be possible for attendees to enroll
and the number of the free seats should be 10
Itt egy képzés meghirdetésekor azt is nézzük, hogy megjelent-e, kiválaszható-e jelentkezésre, és mennyi szabad hely van.
Hiba: komplex lépések
A lépéseket úgy kell megfogalmazni, hogy több forgatókönyvben is újra felhasználhatóak legyenek. Ha túl kompexek, akkor az újrafelhasználhatóságuk korlátozott.
Ez nem fog elsőre menni, ugyanúgy refactoring műveleteket kell végezni. Szerencsére ebben már a modern fejlesztőkörnyezetek tudnak segíteni.
A lépések legyenek egyszerűek és újrafelhasználhatóak!
Hiba: nem megfelelő követelmény, nem megfelelő szinten
Egy megoldás kialakításakor vannak olyan kapcsolódó területek, melyek nem a fő üzleti terület részei, viszont a működéshez elengedhetetlenek. Ilyen például az authentication. Ebben nem is biztos, hogy profik vagyunk. Ennek a DDD is adott nevet, Generic Subdomains néven. Erre általában kész, szabványos megoldások is vannak.
Ha a ChatGPT-t megkérdezzük, nagyon hasonló Gherkin példákat hoz:
Feature: User Login
Scenario: Successful login with valid credentials
Given the user is on the login page
When the user enters a valid username and password
And the user clicks the "Login" button
Then the user should be redirected to the dashboard
Scenario: Unsuccessful login with invalid credentials
Given the user is on the login page
When the user enters an invalid username or password
And the user clicks the "Login" button
Then an error message should be displayed
Scenario: Login attempt with empty fields
Given the user is on the login page
When the user leaves the username and password fields empty
And the user clicks the "Login" button
Then a validation message should be shown for the required fields
Ez hemzseg a hibáktól.
Egyrészt egy üzletileg teljesen lényegtelen követelményt definiálunk.
Nagy fájdalmam a fejlesztésekkel kapcsolatban, hogy rengeteg idő és energia megy
a regisztráció, authentication megvalósítására, ahelyett, hogy kész megoldásokat
használnánk. Mindenki user
táblát hoz létre, jelszókezelést, admininsztrációs
felületeket, erre Gherkin forgatókönyveket, ahelyett, hogy olyan kész megoldásokat
használnánk, mint pl. a Keycloak. Természetesen ezek tesztelése sem a mi dolgunk,
hanem a megoldás szállítójáé (akkor is, ha nyílt forráskódú).
Csak a fő követelményekre, a DDD alapján a core domainre koncentráljunk!
A másik, könnyen felfedezhető hiba, hogy tele van felhasználói felületekre való utalásokkal.
A harmadik, hogy teljesen lényegtelen apróságra is definiál forgatókönyvet (üres mezők). A BDD nem tesztelési szint, azonban integrációs, vagy inkább E2E szinten van értelme a használatának. A tesztpiramis alapján azonban ezekből kevesebbet írunk, mert karbantartásuk, futtatásuk erőforrásigényes.
Formai validációk, hibás esetek ellenőrzését inkább unit, esetleg integrációs teszt szinten valósítsuk meg!
A forgatókönyveknek rövideknek, átláthatóaknak kell lenniük, senki sem szeretné megérteni úgy a rendszer működését, hogy 300 oldalon keresztül azt olvassa, hogy milyen hibás adatok esetén milyen hibaüzeneteket várunk el.
Hiba: nincsenek implementálva a lépések
Ekkor a lépések mögött nincs konkrét kód, gyakorlatilag csak dokumentáció van. A BDD egyik legnagyobb előnye, hogy használatával egy “élő dokumentációt” kapunk, ami azért nem tud elavulni, mert változás esetén nem fut le, hiba keletkezik. Tehát a dokumentáció kikényszerített módon együtt változik a követelményekkel. Ezt veszítjük el, ha elhagyjuk a kódot.
Mindig implementáljuk a forgatókönyveket!
Példa projekt megvalósítási részletei
Egy példa forgatókönyv:
Feature: Enroll in a course
Scenario: Enroll in a course
Given I announce a course with a limit of 3
When John enrolls in the course
Then the number of the free seats should be 2
A projekt érdekessége, hogy egy stephez két step definition is tartozik. Egy REST hívás RestAssured használatával, valamint a felhasználói felület hívása Seleniummal.
Azonban ha közvetlenül akarjuk a .feature
fájlt futtatni, meg kell adni, hogy
melyik ún. glue kódot használja. Az IDEA-ban a Run configurationnél lehet megadni
a csomag nevét, ami vagy courses.restassured
vagy courses.selenium
legyen,
de sosem mindkettő. Persze lehet közvetlenül a CucumberRestAssuredIT
vagy
a CucumberSeleniumIT
osztályt futtatni.
A step definition a CucumberRestAssuredIT
osztályban:
@When("I announce a course with a limit of {int}")
public void iAnnounceACourseWithALimitOf(int limit) {
code = "curse-" + UUID.randomUUID();
var course = with()
.body(new CourseAnnouncement(code, "Java", limit))
.post("/api/courses")
.then()
.statusCode(201)
.extract().as(CourseView.class);
assertEquals(code, course.code());
assertEquals("Java", course.name());
assertEquals(limit, course.limit());
}
Látható, hogy a lényegtelen részeket nem kell megadni,
a kód egy generált egyedi azonosító (az egyediségét az UUID adja),
a név pedig egy beégetett szöveg (Java
). Itt egyedül a
maximális résztvevő szám adható meg.
Az alkalmazásnak REST API-n keresztül, RestAssured használatával küldök be egy kérést, hogy hirdesse meg a képzést. Leellenőrzöm, hogy a művelet sikeresen lefutott-e.
Látható az is, hogy a code
értékét később használni akarom. Ezt elmentem
egy attribútumként definiált változóba, aminek értékét egy másik lépésben
ki lehet olvasni.
A step definition a CucumberSeleniumIT
osztályban:
@When("I announce a course with a limit of {int}")
public void iAnnounceACourseWithALimitOf(int limit) {
code = "course-" + UUID.randomUUID();
coursesPage.announceCourse(code, "Java", limit);
}
Látható, hogy itt is generálja a kódot. De a művelet elvégzését delegálja
a CoursesPage
osztálynak. Az ebben található metódus:
public void announceCourse(String code, String name, int limit) {
driver.findElement(By.name("code")).sendKeys(code);
driver.findElement(By.xpath("/html/body/form[@name='announce-form']/input[@name='name']"))
.sendKeys(name);
driver.findElement(By.name("limit")).sendKeys(String.valueOf(limit));
driver.findElement(By.xpath("//button[text()='Announce']")).click();
new WebDriverWait(driver, Duration.ofMillis(500))
.until(d -> d.findElement(By.cssSelector("div")).getText().contains("Announced"));
}
Azaz Selenium használatával kitölti az űrlapot, elküldi, és még az üzenetet is megvárja, hogy a kurzus meghirdetésre került.