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.