Csak a tesztemen keresztül!

Szakmai gondolatok informatikai rendszerek teszteléséről.

Gazda

Friss topikok

  • Akron: @Verhás Péter: Az ügyfeled a cég (vezérigazgatóval, Józsi nénivel) A cég ellenőrzi le és veszi át ... (2018.01.31. 14:09) Miért UAT
  • Verhás Péter: @ hrgy: Egy kicsit felhúztam magam, de sebaj. A lényegi állításokban igazad van, ám a személyekre... (2011.10.09. 00:25) A tesztelés nem mindenható
  • crix: Ez a hozzáállás. Sajnos most is. Aztán csodálkoztak itt a népek, amikor bevállaltam a hétvégi munk... (2011.10.05. 16:56) Újra fizetni kell
  • crix: és milyen specifikáció mentén ment át az ügyfél? elég blind? (2011.10.05. 13:41) Ügyfélteszt
  • fqqdk: fitnesse, concordion, és cucumber integráció lesz? (2011.03.05. 13:42) Automatizált tesztelés és üzleti tesztelés

Társblogok

Unit tesztelés EasyMock-kal

2010.01.04. 10:00 | Verhás Péter | 17 komment

Ez most egy kicsit erősebben szakmai bejegyzés.

Kell-e unit teszt? (naná)

Korábban már volt szó unit tesztelésről, és felvetettem azt a kérdést is, hogy a unit teszteket vajon a program kód megírása előtt, vagy utána kell elkészíteni. (És kell-e egyáltalán?)

Hogy mostanában volt néhány komolyabb fejlesztési, és nem csak tesztelési illetve szakértői feladat, elkezdtük nagyon szigorúan venni magunkat, és írtuk rendületlenül a unit teszteket. Eleinte csak azért, mert elhatároztuk, hogy de most aztán fogunk, meg mert ezt láttuk az Atlassian kódjában is, és nekik hiszünk. Később viszont azért is, mert sokkal jobban lehetett haladni.

A modulok ki voltak osztva, és amelyikben több unit teszt készült ott bizony kevesebb bugot kellett kijavítani az integrációs tesztek során.

Ha valahol hiba volt, és nem volt unit teszt, akkor írtam egyet. De eleinte nem. Eleinte azt mondtam, hogy ott egye meg a fene, dühös vagyok, aki kódolta a csapatban nem írt unit tesztet, az ő dolga lett volna, én nem írom meg helyette. Na ez hülyeség volt.

Azt gondoltam, hogy kijavítom a hibát, lefordítom, deployolom Tomcat alá, bejelentkezek, és ellenőrzöm, hogy működik. Nem működött. Tomcat újra indít debug módban, debuggolás. Ah.. ja, persze, hiba megvan. Kijavítom a hibát, lefordítom, deployolom Tomcat alá, bejelentkezek, és ellenőrzöm, hogy működik. Nem működött...

Amikor a harmadik hibánál eljátszottam ezt és a ciklus mind a három esetben hat-nyolc hibajavítást jelentett (igen, gyenge vagyok, vagy csak nagyon nehéz más kódját javítani), és a Tomcat is a modulok miatt 30mp alatt indult, szóval másfél vagy inkább két órája szórakoztam, és nem láttam még a végét, hogy mikor indul el végre rendesen a program, akkor kezdtem arra hajlani, hogy ahol lehet kellene írni valami unit tesztet.

A unit teszttel rögtön lehet debuggolni, nem kell a Tomcat indulásra várni, se deploy, se arra hogy a modulok mind betöltődjenek. Meg kell írni és onnan 10mp múlva lehet debuggolni.

Igen, csak volt egy kis bökkenő: a kód egy szervletben futott, és mint ilyen használta a HttpServletRequest változót, amit a http kérésből állít elő a tomcat, a válaszhoz a HttpServletResponse változót, a konfigurációhoz az Apache commons config-ját, szóval ezeket nem olyan egyszerű megteremteni a teszt környezetben.

Persze átírhatom úgy a kódot, hogy ezeket ne használja, direktben, és a tesztelendő részt kiemelem, de nem lenne hatékony, nem lenne szép, és főleg: ne a nyúl vigye a puskát. Van olyan, hogy kódot tesztelhetőre írunk, de azért mindennek van határa.


No de akkor mi a megoldás?


Az első lépés az, hogy a kódot úgy írjuk meg, hogy valóban unitként működjön. Más szavakkal lehessen külön, az általa használt objektumoktól részben függetlenül használni. Ennek egyik módja, hogy minden olyan objektum, amit kívülről használ, az setter-rel beállítható legyen, és ha nem lett beállítva, akkor a getter (amit belülről is használunk és nem a privát mezőt) hozza létre. Például:

private HttpServletRequest req=null;
public void setReq( HttpServletRequest req){
  this.req = req;
  }
public HttpServletRequest  getReq(){
  if( this.req == null ){
    this.req = Context.getContext().getHttpServletRequest();
    }
  return this.req;
  }


A példában a Context egy thread local tár, ahova a servlet doGet és/vagy doPost metódusa helyezi el a servlet konténer által adott  HttpServletRequest változót. Ha servlet konténerben fut, akkor rendesen működik, ahogy kell, ha viszont unit teszt, akkor beállítok egy tesztelésre jó  HttpServletRequest változót.

Eddig az elmélet. Az ördög viszont a részletekben nem alszik. Hogyan lesz nekem egy olyan változóm, amelyik a tesztelésre jó, és implementálja a HttpServletRequest interfészt?

A válasz erre az EasyMock csomag ami az http://easymock.org/ oldalon érhető el. Képes rá, hogy futási időben állítson elő olyan osztályokat, amelyek extendálnak meglevő osztályokat, vagy implementálnak interfészeket, és meg lehet adni, hogy milyen hívásra milyen válaszokat adjanak. Ezeket a válaszokat a teszt előkészítése során határozzuk meg, és a teszt futtatása során adja őket vissza a run-time létrehozott osztály.

Példaként itt a groowiki egy konkrét teszt metódusa:

    public void testPHMethodAbsolute() throws Exception {
        HitContext context = HitContext.getContext();
        HttpServletRequest req =
              EasyMock.createMock(HttpServletRequest.class);
        HttpSession session = EasyMock.createMock(HttpSession.class);
        EasyMock.expect(req.getSession()).andReturn(session);
        EasyMock.expect(req.getPathInfo()).andReturn("/path");
        EasyMock.expect(req.getContextPath()).andReturn("");
        EasyMock.expect(req.getServletPath()).andReturn("");
        EasyMock.replay(req,session);
        context.setReq(req);
        ParameterHandler ph = new ParameterHandler();
        EasyMock.verify(req,session);
        assertEquals("kakukk/../alma is not alma",
              "alma",ph.absolute("kakukk", "../alma"));
    }

Az elején lekérjük a HitContext változót ami thread local, és a paraméter handler innen kéri le a req változót majd. A createMock metódussal hozzuk létre a teszt req és a teszt session objektumokat. Az expect static metódussal mondjuk meg, hogy milyen hívásokat várunk majd a tesztelt modultól, és milyen értéket kell majd visszaadni.

 Amikor ezek megvannak, akkor a replay() metódus jelzi, hogy innen kezdve már nem a teszt definiálása folyik, hanem a teszt maga. A végén a verify() metódus pedig ellenőrzi, hogy minden hívás megtörtént-e amit vártunk.

Tudom, nem ez az egyetlen mock csomag Java-hoz. Kommentekben várom, hogy ki mit szeret. Az általam ismert lista a http://www.mockobjects.com/ oldalról:

  • jMock
  • EasyMock
  • rMock
  • SevenMock
  • Mockito

Vagy éppen a mockejb amit szintén használtunk már.
 

 

 

A bejegyzés trackback címe:

https://csakatesztemenkeresztul.blog.hu/api/trackback/id/tr101637362

Kommentek:

A hozzászólások a vonatkozó jogszabályok  értelmében felhasználói tartalomnak minősülnek, értük a szolgáltatás technikai  üzemeltetője semmilyen felelősséget nem vállal, azokat nem ellenőrzi. Kifogás esetén forduljon a blog szerkesztőjéhez. Részletek a  Felhasználási feltételekben és az adatvédelmi tájékoztatóban.

joco 2010.01.13. 23:51:12

Az első kódrészletben a getReq fv első sora nem hibás? Nem tudom, csak kérdezem. A req változo ott biztosan nullnak tűnik.

verhasi · http://verhas.com 2010.01.13. 23:57:12

@joco: Nem nem hibás. Két azonos nevű változót használunk. Az egyik az osztály szintű és az ott null. A másik a lokális amit paraméterként kap meg a függvény és így eltakarja az osztályszintűt. Szerencsére a this. formulával mégis tudunk rá hivatkozni. Ezért nem hibás, hogy a null értékű változónak értéket adunk.
üdv
vi

joco 2010.01.14. 00:02:23

De hisz nem kapja meg a fv, vagy nem tudok Javaban programozni. (Nem tudok Javaban programozni.)

verhasi · http://verhas.com 2010.01.14. 00:03:59

@joco: Na akkor mégegyszer (az előző válaszom a getReq függvényre vonatkozott volna ha arról kérdeztél volna:) most már tényleg a kérdésedre válaszolva. Nem nem hibás. Az osztály szintű változó a getReq függvény hívásakor vagy null vagy nem. Ha meghívásra került a setReq egy nem null paraméterrel, akkor nem null. Ha nem, akkor null maradt.
üdv
vi

joco 2010.01.14. 00:13:39

De de, a getReq fv-re gondoltam. Szégyen vagy sem, PHP-ban nyomom, ott a req lokális változó lenne csak a fv-en belül (req == null-ról szól a vizsgálat), ha nem adjuk át, mindenképpen null (nem látok paramétereket a függvénynév után, akkor PHP-ban nincsenek is, nem ismerem a Javat), és a this.req lenne az osztályszintű változó, persze hogy.
Szóval a kérdés konkrétan az, hogy nem-e [if (this.req == null)] kéne az a vizsgálat legyen inkább. Elég egy egyszavas válasz.

verhasi · http://verhas.com 2010.01.14. 00:32:20

@joco: nem. Nem kell de ez is jó lenne. A több szavas válasz pedig, hogy a java kicsit szigorúbb mint a php. Ha nincs olyan változó akkor le sem fordul. Itt nem ez a helyzet, hanem a req az osztály változó rövid hivatkozása. Általában ezt szokás használni. A this. típusúra pedig a setReq-ben van példa.

joco 2010.01.14. 00:42:51

Hah, értem! Köszi hogy mégis kifejtetted. Akkor a getReq függvényben akár végig lehetne req a this.req helyett?

verhasi · http://verhas.com 2010.01.14. 00:47:50

@joco: IGEN! Bár nem én írtam de szinte biztos vagyok benne, hogy a copy+paste ördögével van dolgunk....

joco 2010.01.14. 01:09:22

Érdemes volt ezen a threaden végigvernünk egymást, mert a végén csak megértettem. Köszönöm.

joco 2010.01.14. 01:10:54

És akkor most már látom azt is, hogy a gányolási potenciál a Javaban is megvan. :)

Verhás Péter · http://csakatesztemenkeresztul.blog.hu 2010.01.14. 12:30:29

>gányolási potenciál a Javaban is megvan

Igen, de nem sok. Általában nem szoktunk olyan változóneveket használni lokálisan, amilyen nevű változó osztályszinten van. Ez alól tipikusan kivételek a setterek, hiszen a

private TIPUS x;
public void setX(TIPUS x){ this.x = x; }

alak elég olvasható. Meg pl. a NetBeans is ilyent generál. Emiatt a getter-ben is kihangsúlyozandó, hogy osztályváltozót adunk vissza ki szonktuk írni a 'this.' előtagot a változó neve elé, holott nem lenne kötelező.

De nagyon jól kiszúrtad, hogy nem voltam következetes. Egyébként eredetileg működő kódból vettem ki, de a példa kedvéért sallangoktól tisztítottam, így akár még hibás is lehet.

Most már kijavítottam a getReq() metódust. Köszönöm a figyelmedet.

btw: nem akarsz Java-t tanulni? 2001-ben is mondtam neked hogy javadra válik. (Bocs a rossz szóviccért).

tvk · http://kodzaj.blog.hu 2010.01.14. 15:33:35

Azt a this.req-t én inkább példányváltozónak hívnám. Illetve osztályváltozónak akkor hívnám, ha static előtagja van.

fqqdk 2010.01.15. 13:56:46

nem azt mondom, hogy a javaban több lenne a gányolási potenciál, mint a phpban, mert az nem igaz, de azért global state-et nagyon könnyen lehet csinálni, meg procedurális kódot is könnyű írni benne.

misko.hevery.com/ egy nagyon jó blog, leír kábé mindent (itt: misko.hevery.com/code-reviewers-guide/), amivel a mikrotesztelést lehetetlenné lehet tenni. a nehezen (vagy sehogy) nem tesztelhető kód pedig egyben rosszul tervezett kód is (erről nagyon sokan írnak, nemcsak Miško).

van egy ilyen ökölszabály, hogy 3rd party kódot ne mockoljunk, akkor se, ha a mocking eszközünk nagyon okos és ügyes. és 3rd party ilyen szempontból a HttpServletRequest osztály is. célszerű becsomagolni egy saját adapter osztályba, és a kliensnek azt használni.

fqqdk 2010.01.15. 13:57:58

huh, a második link elromlott, mert a zárójelet is beleértette a végén. tudnátok javítani?

Verhás Péter · http://csakatesztemenkeresztul.blog.hu 2010.01.18. 14:56:12

Azért az milyen már, hogy meghivatkozol egy cikket, amelyik leírja, hogy 3rd party kódot ne mockoljunk, és erre ott is pont az első komment arról szól, hogy hát ezzel nem mindenki ért egyet, és éppen a HttpServletRequest az amit a kommentelő mockol.

Never say never.

fqqdk 2010.01.18. 21:10:36

1. Én aztán nem mondtam, hogy soha :) Én azt mondtam, hogy "ökölszabály", amit az angolszász "rule of thumb" helyett használtam, és azt értettem alatta, hogy "általában" (lehet, hogy helytelenül). Eszem ágában sincs kategorikusan kijelenteni, hogy mi jó, meg mi rossz. Pláne mivel egy-két linken kívül nincs más a tarsolyomban. Ez alatt azt értem, hogy nincs konkrét anekdotám, amiben migrénes fejfájást, és egészéjszakás debug szessönt okozott volna egy mockolt HttpServletRequest. De azt nyugodtam mondhatom, hogy eddig csak hasznát láttam a fenti elvnek.
2. A servlet api pont az, amivel kapcsolatban nagyon jól lehet példálózni a vita (mit és mit ne mockoljunk) mindkét felén, mert nyilvánvalóan életszagúbb, mint a Foo meg a Bar, és nagyon sok projektnél jelentkezik dependenciaként :D Azt viszont tudni kell, hogy némely hardcore TDD arcok szerint, mint Miško, borzalmasan rossz API (misko.hevery.com/2009/04/08/how-to-do-everything-wrong-with-servlets/ és itt most nem hiszem, hogy téma szempontjából nagy probléma lenne, hogy ez már nem a Requestről szól).
süti beállítások módosítása