Unit tests should be black box tests - miert is karos a mockolas, es miert kell talalnuk egy jobbat helyette

Gondolom sokan irtunk mar itt unit testet, ki ilyen formatumban, ki olyan modszerrel, de altalaban ugy nez ki a felepitese, hogy 3 csoport van: given, when, then. Altalaban a given reszben definialjuk (rosszabb esetben) a state-et, a dependenciakat es megadjuk, hogy azok hogy is viselkedjenek. A when reszben meghivjuk az aktuallis underTest fuggvenyt, majd a then agban ellenorzunk.

Ugy gondolom, hogy a given-t vagy teljesen el kellene hagyni, vagy legalabbis csak a parameterek inicializaciojara hasznalni. A mockokat ki kell dobni a fenebe. Karosak. Nehezkes oket felsetupolni. Minden egyes dependencia hivashoz meg kell adni a viselkedest. Implementacio fuggove teszi a tesztunket.

Alapvetoen nem azt akarjuk tesztelni, hogy egy adott implementacio hogyan oldja meg a feladatot, hanem arra vagyunk kivancsiak, hogy adott bemenetre a vart kimenet jon-e. Az osszes tobbi csak sallang, boilerplate, szuksegtelen. Alapvetoen interface-re kellene irni a tesztet, hiszen nem az implementacio az erdekes szamunkra, hanem az altala nyujtott funkcio.

Nyilvan szuksegunk van mock objektumokra, hiszen az eles osztalyt behuzalozni meg nagyobb szenvedes lenne, es akkor mar nem csak a unitot teszteljuk. Akkor meg megis hogy lehetne elhagyni?

Ahogy fentebb is emlitettem, a teszt az adott unit viselkedeset irja le bizonyithatoan helyesen. Mig a mock objektum egy altalunk meghatarozott, az eredeti viselkedessel nem feltetlenul azonos viselkedessel rendelkezik. Ez veszelyforras. Adhatok at egy ures listat egy metodusnak, ami listat var anelkul, hogy beleneznek a dokumentacioba, majd mar csak az integracios tesztnel derul ki, hogy jahat arra IllegalArgumentException-t dob.

Tehat a mock nehezkes es veszelyes. Viszont vannak tesztjeink, amik bizonyithatoan helyesek. Miert ne hasznalhatnank oket mock objecteknek? Hiszen arrol van szo, hogy a tesztben leirjuk, hogy (mivelhogy pure functionoket hasznalunk), hogy "ilyen bemenetre olyan kimenetet adj vissza". Ha megforditjuk, "itt egy bemenet, adj egy kimenetet", akkor meg is kaptuk az eredeti fuggvennyel megegyezoen viselkedo mock objectunket.

Azt nyilvan lehet ellenervkent felhozni, hogy B osztaly A-nak adott implementaciojara keszult fel es ha valtozik az implementacio, az mar nem ugyanaz a unit teszt, ujat kell irni, vagy hogy ez mar inkabb integracios teszt, de ezzel nem ertek egyet. Ugyan tekinthetunk ugy is unit tesztekre, hogy adott idopillanatban ervenyes viselkedest irjuk le egy idealizalt kornyezetben, de ez nem hatekony es nehezen karbantarthato, nem mellesleg elvonja a lenyegrol a figyelmet.

El kell hagynunk a mockokat es jobbat kell talalnunk helyette. Meg kell talalnunk a modjat, hogy a tesztjeink masoknak mock-kent felhasznalhatoak legyenek. Ha a teszt es a mock ugyanaz, akkor minel tobbet hasznaljak a mockunkat (tesztjeinket), minel tobb mock esetet (teszt esetet) adnak hozza a meglevokhoz, annal inkabb biztosak lehetunk nem csak az adott unit, de az egesz rendszer mukodokepessegeben.

Lehet, hogy nem mindenhol megvalosithato ez. Biztos van olyan kodbazis, olyan eset amikor ez az ut nem lesz jarhato. Pl. IO-t mindig mockolni kell majd. Lehet, hogy uj paradigmakat kell kitalalni es uj best practiceket. Eloszor csak kicsiben lehet elkezdeni, de ahogy egyre elterjedtebb lesz, ugy lesz egyre konnyebb tesztet, jo, megbizhato tesztet irni. Ami eddig csak szenvedes volt, az onnantol kezdve a leghatekonyabb eszkoz lesz, onmagat erositoen eredmenyezne jobb kodminoseget.

Hozzászólások

Nagyon jó gondolatok vannak a blogodban és alapvetően egyetértek vele.
Az általam linkelt blogban szereplő videóban is felmerülnek hasonló gondolatok, érdemes megnézni.
A blogomban szereplő hozzászólásokat is érdemes elolvasni, mert sok okos gondolat van benne.

Nézzük most ezt a blogot részletesebben!

A mockokat ki kell dobni a fenebe. Karosak. Nehezkes oket felsetupolni. Minden egyes dependencia hivashoz meg kell adni a viselkedest. Implementacio fuggove teszi a tesztunket.

Pontosan, egyetértek teljes mértékben!

Viszont vannak tesztjeink, amik bizonyithatoan helyesek. Miert ne hasznalhatnank oket mock objecteknek?

Azért, mert tulajdonképpen azok is mock-ok és egyáltalán nem igaz, hogy bizonyíthatóan helyesek lennének. A bizonyítás nélküli helyesség is csak akkor lenne igaz, ha lenne rájuk teszt írva (teszt a tesztre).

El kell hagynunk a mockokat es jobbat kell talalnunk helyette.

Így van, van is rá megoldás, csak ahhoz másféleképpen kell terveznünk, felépítenünk a kódot. Tehát az nem megoldás, hogy ugyanaz a kódunk marad, csak más mock-ot használunk.

Pl. IO-t mindig mockolni kell majd.

Azt sem kell mockolni, egyrészt lehet fake-elni, másrészt áttervezéssel a legtöbb helyről ki lehet szorítani, egészen a legfelső szintig.

Lehet, hogy uj paradigmakat kell kitalalni es uj best practiceket.

Igen, ez a megoldás, a videóban ad is egy pár példát erre.

Szerintem amiről írsz az létezik.

Amit én ebből a cikkből megértettem, hogy kétfajta vallás/iskola létezik ha Unit tesztelni akarunk.

Az egyik amiről írod, hogy nem jó és nem tetszik a "Mockist Testing", a másik pedig ami neked tetszene az a "Classical Testing".

Ajánlom az egész cikk végigolvasását, mert hiánypótló a témában! :)

Ket dolgot ajanlok figyelmedbe:
http://rbcs-us.com/documents/Why-Most-Unit-Testing-is-Waste.pdf
(masodik resz: http://rbcs-us.com/site/assets/files/1191/segue.pdf)

https://youtu.be/KtHQGs3zFAM

Elsore fejen ut, de minel tobbet agyalok rajta, annal inkabb igazat adok Jim Coplien-nek. Annyira, hogy lassan komoly osszetuzesekbe keveredek a kollegakkal, akik abszolut TDD maniasok, es imadjak a tautologikus unit teszteket.

A mockoknak abban van szerepuk szerintem, hogy uzleti funkciokra levalasztva tudod (funkcio/integracio) tesztelni a modulodat. Vagyis, peldaul tesztelned az authentikaciot, hogy 10 sikertelen belepesi kiserlet utan lockolja az accountot. Tegyuk fel, hogy ez az authorizaciot nem erinti. Igen, de az authentikacio es az authorizacio altalaban egy blokkban van, nehez kulon tesztelni - ezert van a mock, ami az authorizacios reszek helyett fut (peldaul a jogokat egy masik adatbazis tarolja, vagy sok plusz extra informacio kell peldaul a user-group hierarchiarol), az authentikacios tesztek viszont amennyire lehet, a valoshoz hasonlo kornyezetet kapnak, peldaul valos adatbazis helyett in-memory verziot.

Ezt viszont nem ertem: "Viszont vannak tesztjeink, amik bizonyithatoan helyesek."
Mennyivel konnyebb a tesztrol bizonyitani, hogy helyes, mint a kodrol? Adott esetben meg nehezebb is... Tovabb rontja a minoseget, ha a tesztet ugyanaz irja, aki a kodot (ez ugye eleg sokszor elofordul), igy a tesztek - foleg a tautologikus tipusu unit tesztek - altalaban onigazolo jelleguek, tenyleges informaciot nem hordoznak, csak a kodmeretet novelik.

Koszi szepen mindenkinek, mostmar tenyleg raszanom magamat egy belsos eloadasra, mert mar elegem van belole, hogy mindenki a unit teszteket nyomatja, az en allaspontomat meg nem ertik meg, mert a komplex temat kis adagokban nem lehet atadni. Szoval most azt hiszik, hogy azert utalom a mockokat, mert nem ertek a teszteleshez, a csapatom meg elkuldott TDD eloadasra is, hatha meggyoznek :)

Irtam is nekik Slack-en, hogy tok jo, hogy elkuldtetek, eddig semmiben nem mondtak nekem ellent... meg azt se gyoztem hangsulyozni, hogy nem a unit tesztek, meg a TDD, hanem a mockok ellen vagyok.

Ugyan nem olvastam meg vegig az osszeset, de azert megnyugtato, hogy masok is ezen a velemenyen vannak, illetve jo forras lesz az eloadashoz, nem kell mindent fejbol leirkalnom :) Mondjuk valoszinuleg lesz egy-ket dolog, amit mashogy kepzelek majd el a tobbiekhez kepest, na meg arrol se art szot ejteni, hogy egy hogy jon ossze clean code-dal, hogy erdemes felepiteni egy projectet, hogy jol tesztelheto legyen, hogy az IO-t kell mockolni (en meg a Date-et is ide sorolnam, az is egy syscall az OS fele altalaban), mast nem,

Hogy az IO mockolas segitsegevel hogy tudjuk megvalositani a 100%-os verifikaciot. Minden almom az, hogy a kod belso szerkezetet is BDD szeruen (a lenyeget tekintve, termeszetesen nem akarok Gherkinnel szorakozni, API tesztelesre amugyis szornyu volt) lehessen tesztelni, ahol az szamit, hogy mit csinal a fuggveny, nem az, hogy hogyan csinalja.

Ezzel baromi konnyen meg lehet valositani olyan teszteket, hogy "ez a kiindulo feltetel, ezt csinaljuk, ennek ez az eredmenye, ekozben ilyen interakciokat varok a kulvilaggal...".

Erdekel az, hogy egy Service-t hivott meg a fuggveny, vagy egy Facade-t? Nem. Erdekel, hogy az adatokon milyen konverziot vegez? Nem.

Mi erdekel? Az, hogy mi lett a vegeredmeny, es kozben milyen hatasok tortentek a kulvilagra. A belso folyamatok tok nem lenyegesek. Black/white box teszt az igazi :)

White-box testing can be applied at the unit, integration and system levels of the software testing process.

https://en.wikipedia.org/wiki/White-box_testing

Nincs azzal se gond. Ha megfeleloen van felepitve a kodbazis, akkor igazabol minden osztalyt/metodust is le lehet tesztelni, ugyanugy, mintha unit teszt lenne, csak annyi a kulonbseg, hogy az IO-n kivul mast nem mockolunk.

Ezzel megorizhetjuk a unit tesztek nagy elonyet, azt, hogy pontosan mutatjak hol torik a kod. (Szerk: ehhez nagyon jol jonne, ha lenne olyan teszt framework, ami eloszor felepiti az object/call fat/graphot es utana alulrol felfele kezdene el tesztelni. Igy rogton meglenne az az elso integracios teszt ami torik, nem kellene keresgelni se). Nyilvan ennek nagyobb a karbantartasi igenye, mint egy tisztan black-box (BDD) tesztnek, viszont sokkal kisebb, mintha mindent mockolnank.

Ezzel a megoldassal lesz amugy annak ertelme, hogy a tesztek gyakorlatilag az adott fuggveny elo dokumentacioja, mert a figyelmet nem viszi el az implementacios reszlet, azaz a mockok, hanem annak a viselkedesere tud a teszt koncentralni.

A White box teszt az az implementációt teszteli, mit hogyan csinál az adott kód.
Ellentétben a Black box teszttel, ami az teszteli, hogy milyen bemenetre milyen kimenetet ad válaszul.

Mind a két tesztet lehet a tesztelés bármelyik szintjén alkalmazni.

--
Ickenham template engine