Reddit: Egy kis teszt, egy kis kódolás

Írta: Dan Bunea

Egy kis teszt, egy kis kódolás: gyakorlati bevezetés a TDD-be, vagyis a Tesztelésen Alapuló Programozásba

Előszó

A Tesztelésen Alapuló Programozás (angolul TDD) az egyik legelterjedtebb gyakorlat az Agilis közösségek körében. (az Agilis Programozás egy pár éves irányzat, több programozási módszertan is ide tartozik, például az Extreme Programming, de erről majd máskor - a ford.) Viszont szerintem nem sokan értik, hogy miről is szól pontosan, mert azt látom, hogy egy csomóan félreértik és rosszul használják a kifejezést.

A legnagyobb félreértések a TDD-vel kapcsolatban

Sokan, akik nem ismerik az Agilis programozási elveket, vagy kezdők benne, azt hiszik, hogy a TDD annyiból áll, hogy automatikus teszteket írsz a programod kipróbálására, vagy ami még rosszabb, hogy a TDD a program kipróbálására szolgál, és nem pedig a fejlesztésére.

1. A TDD a tesztelőknek szól

Ez a kavarodás a TDD nevéből származik: Tesztelésen Alapuló Programozás, ami sok szoftverfejlesztőnek azt jelenti, hogy programot írunk a tesztelésre is, ami valójában a minőségellenőrző csapat dolga lenne. Amiatt, hogy a 'tesztelés' szó szerepel a nevében, sokan azt hiszik, hogy ez a lényege, tehát nem is kell a fejlesztőknek foglalkozni vele, mert majd a tesztelők elintézik. Ezt azok hiszik, akik csak a nevét ismerik, de semmit se tudnak róla (figyelsz, Shenpen?! - a ford.) Holott a cím elég egyértelmű: automatikus tesztek, amikre a programozás alapul.

2. Én TDD-t használok, mert a programomat automatizált módon tesztelem.

Akik most ismerkednek az Agilis technikákkal, gyakran azt hiszik, hogy egy automatizált tesztsorozat, ami a kód minden részét leteszteli, egyet jelent a TDD-vel. Ez valójában nem is nagy tévedés, csak a dolog lényege pont az, hogy a teszteket a program megírása ELŐTT kell elkészíteni, és nem pedig utána. Ha a tesztprogram megírásakor nem változott meg a tesztelendő program, akkor ez nem TDD, hanem csak automatizált tesztelés, ami persze szintén nem rossz dolog.

Az automatizált tesztek megléte legalábbis arra feljogosít, hogy részlegesen TDD-nek nevezhesd a módszert, még akkor is, ha a teszt később készült el, mint a program maga, mivel magasabb szinten, vagy pontosabban projekt-szinten, az a TDD lényege, hogy kiderüljön, ha valami elromlott attól, amit egy fejlesztő csinált, mert esetleg olyan koncepcionális vagy gyakorlati változásokat von a változtatás magával, amik a program alapjainak megváltoztatását igénylik.

Az elmélet

A Tesztelésen Alapuló Programozás egy szoftverfejlesztési technika, ami szoftverfejlesztőknek, és nem pedig szoftvertesztelőknek készült, és arra alapszik, hogy először automatikus tesztet kell csinálni egy adott dologra, és utána annak megfelelően írni meg magát a kódot. A kód elsőre nem, de végül átmegy a teszten, ami azt jelenti, hogy azért lett a kód olyan, amilyen, hogy a teszten átmenjen.

Ezt a technikát elég sokan leírták már, úgyhogy nem is akarok sok szót vesztegetni a bemutatására. Nevezik amúgy piros-zöld faktornak is. Egy kis teszt, egy kis kódolás, nem jó, megint egy kis kódolás, és máris jó. Ezekből a lépésekből áll alapvetően az egész. Tehát ez teljesen programozási technika, csak kicsit más. Összefoglalva a lépéseket:

1. Teszt: írj meg egy tesztet. Csak egy dolgot tesztelj, sose többet.
2. Juttasd el odáig a kódot, hogy leforduljon, de ne törődj még a teszttel.
3. Futtasd le a kódot. Ha hibát adott a teszt, akkor jó. (piros faktor)
4. Programozz: írd meg a kódot úgy, hogy átmenjen a teszten, de semmi többre ne legyen képes.
5. Teszt: Futtasd le a tesztet, át kell mennie rajta. Ha nem, akkor kicsit kódolj még, menj vissza a 4-es lépésre. Ha átment, zöld jelzést kapsz, ez a zöld faktor.
6. Átdolgozás: dolgozd át a kódot, amit eddig írtál, a tesztet is meg a tényleges kódot is.
7. Teszteld újra. Ha nem megy át, menj a 4-es lépésre, és javítsd ki.
8. Egy lépés megvolt, menj vissza az 1-es lépésre, és írd meg ugyanígy a következő funkciót.

Ez a lehető legegyszerűbb módja a lépések leírásának. A teljes lista ennél hosszabb, mert az 1. lépés jelentős részben gondolkodásból áll, először ki kell találnod, hogy mit is akarsz, aztán azt, hogy hogyan lehet azt a lehető legegyszerűbben letesztelni, és aztán lehet továbblépni a következő lépésekre. Elég sokszor vissza is kell ugrani, hogy az eredmény jó legyen. Ez egy iteratív eljárás, aminek az a lényege, hogy először tesztelsz egy kicsit, aztán kódolsz egy kicsit, amíg a teszten át nem megy, aztán ezt ismételgeted, amíg mindennel kész nem vagy.

Gyakorlat

Tegyük fel, hogy a megrendelő kért egy rendelési rendszert, ami árengedményt ad bizonyos összegű megrendelés esetén az egész rendelésre. Az Extreme Programming szerint ez egy 'felhasználói sztori' leprogramozása lenne.
Azt is tegyük fel, hogy eddig egy sor kódot se írtunk. Először szerintem egy Megrendelés osztály kellene, meg alá egy MegrendelésSor osztály. Írok erre egy tesztet (1. lépés):


[Teszteléshez]
public class MegrendelesTeszt
{
   [A teszt-osztaly]
   public void UresTeszt()
   {
      Megrendeles megrendeles = new Megrendeles();
      [Az Assert kb. 'ellenorizd']
      Assert.Egyenlo(0,megrendeles.Osszeg);
   }
}

De még nincs Megrendeles osztályom. A második lépésben viszont el kell érnem, hogy a kód leforduljon, de a teszt elbukjon. Úgyhogy ezt írom:


public class Megrendeles
{
   public decimal Osszeg
   {
      get { thow new NincsMegirvaException(); }
   }
}

Most már lefordul, úgyhogy lássuk, mi lesz belőle:

Eddig könnyű volt, most a harmadik lépésben átírom úgy a kódot, hogy átmenjen a teszten:


public class Megrendeles
{
   public decimal Osszeg
   {
      get { return 0; }
   }
}

Azáltal, hogy mindig nullát adok vissza, a kód használhatatlan, de átmegy a teszten. Viszont ez csak egy példa, ami remekül megvilágítja azt, hogy a lényeg az, hogy a kód átmenjen a teszten, és ha az ember folyamatosan erre koncentrál, akkor ez fogja az egész fejlesztést meghatározni, tehát sosem szaladunk át semmin.
Most lássuk, hogy átmegy-e, az ötödik lépésben:

Király, most már átmegy a teszten, nézzük, kell-e átdolgozni valamit. Mivel a kód most még túl egyszerű ehhez, ezért ezzel nem kell foglalkozni, és az első iterációval meg is vagyunk. Most vissza az első lépéshez, adjunk hozzá megrendelés-sorokat.


[Teszteléshez]
public class MegrendelesTeszt
{
   [Teszt]
   public void UresTeszt()
   {
      Megrendeles megrendeles = new Megrendeles();
      [Az Assert kb. 'ellenorizd']
      Assert.Egyenlo(0,megrendeles.Osszeg);
   }
   
   [Teszt]
   public void KetSorOsszege()
   {
      Megrendeles megrendeles = new Megrendeles();
   
      MegrendelesSor ms1 = new MegrendelesSor();
      ms1.Termek = "Laptop";
      ms1.Mennyiseg = 1.0;
      ms1.Ar = 1000.0;
   
      MegrendelesSor ms2 = new MegrendelesSor();
      ms2.Termek = "Monitor";
      ms2.Mennyiseg = 2.0;
      ms2.Ar = 200.0;
   
      megrendeles.HozzaadSor(ms1);
      megrendeles.HozzaadSor(ms2);
   
      Assert.Egyenlo(1400.0, megrendeles.Osszeg);
   
   }
}

Csináljuk meg addig, hogy leforduljon, tehát írjuk meg a MegrendelesSor osztályt és a HozzaadSor függvényt:


public class MegrendelesSor
{
   public string Termek
   {
      get { throw new NincsMegirvaException(); }
   }
   
   public decimal Mennyiseg
   {
      get { throw new NincsMegirvaException(); }
   }
   
   public decimal Ar
   {
      get { throw new NincsMegirvaException(); }
   }
}

public class Megrendeles
{
   public decimal Osszeg
   {
      get { return 0; }
   }
   
   public void HozzaadSor(MegrendelesSor ms)
   {
   }
}

Lássuk, elhasal-e:

Akkor most az jön, hogy csináljuk meg, hogy átmenjen a teszten.
Kicsit kódolunk, aztán ha lefordul, megnézzük, átmegy-e a teszt. Ha nem, akkor kódolunk még egy kicsit, és megint megnézzük, addig, amíg át nem megy. Ez lesz belőle:


public class Megrendeles
{
   private IList megrendelesSorok = new TombLista();

   public decimal Osszeg
   {
      get
      {
         decimal ossz = (decimal) 0;
         foreach(MegrendelesSor ms in this.megrendelesSorok)
         {
            ossz += ms.Osszeg;
         }
         return ossz;
      }
   }
   
   public void HozzaadSor(MegrendelesSor ms)
   {
      this.megrendelesSorok.Hozzaad(ms);
   }
}

És a MegrendelesSor:


public class MegrendelesSor
{
   private decimal mennyiseg;
   private decimal ar;
   private string termek;
   
   public string Termek
   {
      get { return termek; }
      set { termek = value; }
   }
   
   public decimal Mennyiseg
   {
      get { return mennyiseg; }
      set { mennyiseg = value; }
   }
   
   public decimal Ar
   {
      get { return ar; }
      set { ar = value; }
   }
   
   public decimal Osszeg
   {
      return this.mennyiseg * this.ar;
   }
}

Nahát, már másodjára sikerül tíz perc alatt :) Lássuk, van-e átdolgoznivaló. Maga a kód rendben van, de a teszttel kezdhetnénk valamit, mert kétszer inicializálja a Megrendeles-t. Át kéne ezt rakni valami Kezdoertek nevű eljárásba, mert az úgyis lefut minden más függvény előtt, meg a MegrendelesSor-t is elég hasonlóan gyártjuk, arra is írjunk függvényt.


[Teszteléshez]
public class MegrendelesTeszt
{
   [Teszt]

   Megrendeles megrendeles = null;

   [Kezdoertek]
   public void Kezdoertek()
   {
      megrendeles = new Megrendeles();
   }
   
   public void UresTeszt()
   {
      [Az Assert kb. 'ellenorizd']
      Assert.Egyenlo(0,megrendeles.Osszeg);
   }
   
   [Teszt]
   public void KetSorOsszege()
   {
   
      MegrendelesSor ms1 = UjMegrendelesSor("Laptop", 1, 1000);
      MegrendelesSor ms2 = UjMegrendelesSor("Monitor", 2, 200);
   
      megrendeles.HozzaadSor(ms1);
      megrendeles.HozzaadSor(ms2);
   
      Assert.Egyenlo(1400.0, megrendeles.Osszeg);
   
   }
   
   private MegrendelesSor UjMegrendelesSor(string termek, decimal mennyiseg, decimal ar)
   {
      MegrendelesSor ms = new MegrendelesSor();
   
      ms.Termek = termek;
      ms.Mennyiseg = mennyiseg;
      ms.Ar = ar;
   
      return ms;
   }
}

Na lássuk, átmegy-e:

Átmegy, úgyhogy a kód jónak látszik, és megvagyunk a második iterációval is, többé-kevésbé letesztelve. Akkor most adjuk hozzá a kedvezményes részt, ugyanígy az első lépéstől:


[Teszteléshez]
public class MegrendelesTeszt
{
   [Teszt]
   
   Megrendeles megrendeles = null;
   
   [Kezdoertek]
   public void Kezdoertek()
   {
      megrendeles = new Megrendeles();
   }
   
   public void UresTeszt()
   {
      [Az Assert kb. 'ellenorizd']
      Assert.Egyenlo(0,megrendeles.Osszeg);
   }
   
   [Teszt]
   public void KetSorOsszege()
   {
   
      MegrendelesSor ms1 = UjMegrendelesSor("Laptop", 1000);
      MegrendelesSor ms2 = UjMegrendelesSor("Monitor", 2, 200);
   
      megrendeles.HozzaadSor(ms1);
      megrendeles.HozzaadSor(ms2);
   
      Assert.Egyenlo(1400.0, megrendeles.Osszeg);
   
   }
   
   [Teszt]
   public void Engedmeny2000Felett()
   {
      ISzabaly kedvezmenySzabaly = new KedvezmenySzabaly(10, 2000);
   
      MegrendelesSor ms1 = UjMegrendelesSor("Laptop", 1000);
      MegrendelesSor ms2 = UjMegrendelesSor("Monitor", 2, 200);
      MegrendelesSor ms3 = UjMegrendelesSor("MiniMac", 2, 500);
   
      megrendeles.HozzaadSor(ms1);
      megrendeles.HozzaadSor(ms2);
      megrendeles.VonatkozoUzletiSzabaly(kedvezmenySzabaly);
   
      Assert.Egyenlo(2400.0, megrendeles.Osszeg);
      Assert.Egyenlo(2160.0, megrendeles.KedvezmenyesOsszeg);
   
   }
   
   private MegrendelesSor UjMegrendelesSor(string termek, decimal mennyiseg, decimal ar)
   {
      MegrendelesSor ms = new MegrendelesSor();
   
      ms.Termek = termek;
      ms.Mennyiseg = mennyiseg;
      ms.Ar = ar;
   
      return ms;
   }
}

Miután mindent megírtunk úgy, hogy a teszt elbukjon, aztán úgy is, hogy átmenjen, amit most itt nem részletezek tovább, azt fogjuk tapasztalni, hogy a teszt nem hajlandó átmenni.

Van egy bugunk a tesztben. Utánanézve kiderül, hogy a harmadik megrendeléssort elfelejtettük hozzáadni, úgyhogy gyorsan adjuk hozzá, és futtassuk le a tesztet. Ebből kiderül, hogy ez a tesztelősdi segít kijavítani a kód hibáit is, meg a tesztprogram hibáit is:

Nos, úgy tűnik, sikerült olyan tesztet csinálnunk, amik segítettek a kód megírásában. Ha ezeket a teszteket hozzátesszük a projekt közös tesztjéhez, és rendszeresen futtatjuk őket, akkor elég jól meg tudjuk majd mondani, hogy mi nem jó a kódban és hol.
Remélhetőleg ez a pár sor oszlatta kicsit a ködöt a TDD körül, és megvilágította, hogy pontosan mire való.

Forrás:
http://bloggingabout.net/blogs/danbunea/archive/2005/12/07/10480.aspx

Fordította: Wintermute