ITHub

A TDD (Test Driven Development) világa, III. rész: Egy egyszerű példa

A TDD (Test Driven Development) világa, III. rész: Egy egyszerű példa
Farkas Gábor
Farkas Gábor
| ~5 perc olvasás

A sorozat előző részeiben egy bevezetőt adtunk a TDD-hez, és eloszlattunk pár tévhitet a módszerrel kapcsolatban. A mai posztban egy nagyon egyszerű példán keresztül mutatjuk be a TDD folyamatot.

A TDD (Test Driven Development) világa, III. rész: Egy egyszerű példa

Ehhez a kis demonstrációhoz egy nagyon közismert TDD példát fogunk felhasználni, ez pedig nem más, mint egy string alapú számológép osztály megírása. Nézzük az alkalmazásunk követelményeit:

  • Az osztálynak (legyen a neve StringCalculator) van egy add() metódusa, ami egy stringet vár, amely 0, 1, vagy 2 vesszővel elválasztott számot tartalmaz (pl. "54,92", vagy "11").
  • Az üres stringre a metódus nullát ad vissza.
  • Más esetben a metódus a számok összegét adja vissza.

Kezdjünk is neki! A példát Java nyelven fogjuk elkészíteni, és a JUnit frameworkot használjuk a tesztekhez. Ennek mélyebb ismerete a példák megértéséhez nem szükséges, de ha nem vagy ismerős a unit teszt keretrendszerek működésével általában, érdemes elolvasni egy tutorialt (például ezt).

Induljunk ki az első követelményből, és készítsük el hozzá a teszteket:

public class StringCalculatorTest {
  @Test(expected = RuntimeException.class)
  public final void whenMoreThan2NumbersThenException() {
      StringCalculator.add("1,2,3");
  }

  @Test
  public final void when2NumbersThenNoException() {
      StringCalculator.add("1,2");
      Assert.assertTrue(true);
  }

  @Test(expected = RuntimeException.class)
  public final void whenNonNumberThenException() {
      StringCalculator.add("1,X");
  }
}

Az első teszttel azt fogalmazzuk meg, hogy a metódus nem fogadhat kettőnél több számot tartalmazó stringet - ebben az esetben az elvárt viselkedés egy kivétel. A második teszt azt ellenőrzi, hogy amennyiben a metódus pontosan két számot kap, nem kapunk kivételt (ezt úgy oldjuk meg, hogy a hívás után egy mindig igaz elvárást alkalmazunk, így ha nem volt exception, ez biztosan le fog futni). Végül a harmadik tesztünk azt ellenőrzi, hogy amennyiben olyan stringet adunk át, ahol a listában nem csak szám szerepel, szintén kivételt kell kapnunk.

Készítsük el a legegyszerűbb implementációt, ami mindhárom tesztet igazzá teszi.

public class StringCalculator {
  public static final void add(final String numbers) {
    String[] numbersArray = numbers.split(",");
      
    if (numbersArray.length > 2) {
        throw new RuntimeException("Maximum 2 szam!");
    } else {
        for (String number : numbersArray) {
            // Ha a bemenet nem szam, a parseInt exceptiont fog dobni
            Integer.parseInt(number);
        }
    }
  }
}

Ezt lefuttatva mindhárom tesztünk sikeresen le fog futni ("green" lépés). Mivel sok refaktorálni valót nem találunk, mehetünk tovább a következő követelményre. Az alábbi új teszt azt hivatott ellenőrizni, hogy egy üres stringre az eredmény 0 lesz-e:

@Test
public final void whenEmptyStringThenReturn0() {
  Assert.assertEquals(0, StringCalculator.add(""));
}

Ha újra lefuttatjuk a tesztjeinket, a legújabb természetesen piros lesz, hiszen még nem implementáltuk az új funkciót. Tegyük meg ezt is:

public static final int add(final String numbers) {
  String[] numbersArray = numbers.split(",");
  
  if (numbersArray.length > 2) {
    throw new RuntimeException("Maximum 2 szam!");
  } else {
    for (String number : numbersArray) {
        if (!number.isEmpty()) {
            Integer.parseInt(number);
        }
    }
  }
  
  return 0;
}

Érdemes megfigyelni, hogy ismét csak a minimumra törekedtünk ahhoz, hogy minden teszt zölddé váljon. Ehhez egyrészt a függvény visszatérési típusát void helyett int-re változtattuk, illetve hozzáadtuk a return 0; sort, amivel teljesítettük is a tesztet — egyelőre nem kell azzal törődnünk, hogy a későbbi követelmények fényében ez az implementáció nem megfelelő.

A harmadik követelményünk az, hogy ettől eltérő esetekben a metódusnak a számok összegét kell visszaadnia. Első lépésben ismét írjuk meg a teszteket. Két esetet fogunk vizsgálni, az egy és a két számot tartalmazó bemeneteket.

@Test
public final void whenOneNumberThenReturnSame() {
  Assert.assertEquals(3, StringCalculator.add("3"));
}
 
@Test
public final void whenTwoNumbersAreUsedThenReturnSum() {
  Assert.assertEquals(3+6, StringCalculator.add("3,6"));
}

Az első esetben nyilván azt várjuk, hogy a visszatérési érték ugyanaz lesz, mint a bemenet, hiszen csak egy számunk van. A második esetben az összeget szeretnénk visszakapni. Az összes tesztet ismét lefuttatva a fenti kettő nyilván nem fog teljesülni; készítsük el ismét a minimális implementációt.

public static int add(final String numbers) {
  String[] numbersArray = numbers.split(",");
  
  if (numbersArray.length > 2) {
    throw new RuntimeException("Maximum 2 szam!");
  }
  
  if (numbersArray.length == 1) {
    return numbersArray[0];
  }
  
  if (numbersArray.length == 2) {
    return numbersArray[1]+numbersArray[0];
  }
  
  return 0;
}

Ez az implementáció már zölddé teszi az összes tesztünket, azonban látható, hogy nem túlságosan jó. A "green" fázisban ez nem érdekelt minket, hiszen a lényeg csak az volt, hogy a teszt sikeres legyen, azonban ideje áttérnünk a "refactor" lépésre, és tegyük ésszerűbbé, elegánsabbá a metódusunkat:

public static int add(final String numbers) {
  int returnValue = 0;
  String[] numbersArray = numbers.split(",");
  
  if (numbersArray.length > 2) {
    throw new RuntimeException("Maximum 2 szam!");
  }
  
  for (String number : numbersArray) {
    if (!number.trim().isEmpty()) {
        returnValue += Integer.parseInt(number);
    }
  }
  
  return returnValue;
}

Refaktorálás után egy sokkal kompaktabb metódusunk van, és a tesztek újrafuttatásával biztosak lehetünk abban, hogy az átírás ellenére még mindig pontosan ugyanazt csinálja, mint azelőtt — ez a TDD és a unit tesztelés legnagyobb ereje.

A példát még lehet természetesen további követelményekkel bonyolítani, a célunk itt csupán az volt, hogy bemutassuk a folyamatot. El lehet képzelni, hogy ha ez az "alkalmazás" tovább bonyolódna, akár úgy is dönthetünk, hogy több osztályra és metódusra bontjuk az egész megoldást, az addig megírt tesztek védőhálót fognak nyújtani, így bátran szinten tarthatjuk a kódminőséget folyamatos refaktorálással.