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.