Mock & Stub, avagy hogyan tesztelünk rosszul tervezett kódot

A napokban találtam a témában egy nagyon jó videót Ken Scambler-től.

A videóban szépen sorra veszi, hogy milyen tervezési hibák miatt használunk Mock-ot és Stub-ot, valójában ezek mit szeretnének kiváltani; és milyen módon tervezhetjük át a kódot, hogy ne legyen szükség rájuk, aminek hatására:

  • modulárisabb,
  • jobban újrafelhasználható,
  • egyszerűbb,
  • kevesebb kód,
  • kevesebb változó rész,
  • Mock eltűnik,
  • Stub eltűnik

A videóban Java nyelvű példakódok szerepelnek. Ha valaki jobban szereti a szöveges információt (vagy a Scala példákat), akkor itt, a hasonló témát el is olvashatja.

"Testing without mocking in Scala" Jessica Kerr-től szintén hasznos olvasmány lehet a témában (szintén Scala).

Szerk.:
Egy újabb kincsre bukkantam a témában (szintén Scala): Avoid Javaisms: Mocks, Stubs, DI is Code Smell, írta: Alexandru Nedelcu

Szerk.:
Test-induced design damage or why TDD is so painful cikk sorozat, Vladimir Khorikov.
Mocking is a Code Smell by Eric Elliott (Javascript)
No Mocks cikkek, Arlo Belshee
Testing Difficult Code by Llewellyn Falco

Hozzászólások

Nagyon sokszor az ember nem akar egy komplett webszervizt leemulalni (pl. HTTP endpointokat), ott sokkal jobb a mockolas, mintha szetmodularizalod az egeszet - csak azert, hogy a teszteles szebb legyen. Mert annak mas hozama nemigen van.
--
Blog | @hron84
Üzemeltető macik

Hat, az elegge kerdeses. Most nezem a videot, es emberunk elegansan kihagyta a UserService-s pelda megoldasat, a PotatoServices-rol meg orditott a rossz tervezes. Fix ertekeket, meg egy objektumon beluli hivogatasokat en is kirefaktoralok neked percek alatt (pedig nem ertek olyan nagyon a kodolashoz), de amikor tobb kulonallo service-nek kell osszedolgozni, azt mar nem olyan trivialis kibanyaszni, foleg ugy, hogy a tul sok parameter egy konstruktoron/metodushivason megint csak rossz. Azt mar meg sem emlitem, amikor az aktualis szerviz az valojaban csak valami interfesz, ami mogott szinte barmilyen szolgaltatas allhat.

Gondolom, hogy a UserService-s pelda megoldasa valami fixturas tortenet bevezetese, vagy egy actual tesztadatos adatbazis felepitese, de hat ugye a mock-okat pont azert vezettuk be, hogy a unit tesztek szintjen meg ne kelljen rendes tesztadat.

Nem tudom, szerintem meg kell hatarozni egy hatart, ameddig el kell menni az ilyen coding detail-eken valo lovaglason, mert barmifele bonyolitasa a kodnak idokoltseggel jar.

Ami meg a stubokat illeti, egy audit rendszert pl. nemigen tesztelsz nelkuluk, hiszen ott az aktualis output kvazi irrelevans (a kimenetet el fogod dobni, mert senkit nem erdekel), az a lenyeg, hogy az audit rendszerbe leuti-e az app a megfelelo metodust (ami majd lelogolja az aktualis muveletet).
--
Blog | @hron84
Üzemeltető macik

A UserService-s példa megoldása az lehet, hogy a UserService konstruktorban megkapja az AuthService és UserRepo interface-eket. A tesztnél pedig készítesz hozzájuk egy nagyon egyszerű implementációt.

"Nem tudom, szerintem meg kell hatarozni egy hatart, ameddig el kell menni az ilyen coding detail-eken valo lovaglason, mert barmifele bonyolitasa a kodnak idokoltseggel jar."

Ez nem bonyolítása a kódnak, hanem pont az egyszerűsítése.

> egy nagyon egyszerű implementációt


@Override
public Integer getRandomNumber() { return 4; }

Vagy mi?
Tippre senki nem ír direkt bonyolult production kódot - tehát a legegyszerűbb helyes működés, az már megvan, s pont szeretnénk kiiktatni a kódból. A nagyon egyszerű implementáció pedig gy.k. ugyanaz, mintha kimockolnám.

(nem néztem még meg a videót, de ez a mondat kicsit így fals nekem)
--
blogom

Pontosan, vagy ha a videós példát nézzük, akkor pl. lehet egy ilyen:


public class TestAuthService implements AuthService {
  public boolean authUser(String userId) {
    return userId.equals("1234");
  }
  public boolean authLocalUser(String userId) {
    return authUser(userId);
  }
}

"Tippre senki nem ír direkt bonyolult production kódot"

Pedig de :) Általában elsőre mindenki bonyolult kódot ír. Egyszerű kódot nehezebb írni. Az egyszerű kód nem azt jelenti, hogy egyszerű megírni, hanem azt, hogy egyszerű megérteni, újrafelhasználni, átírni.
Az egyszerűségről bővebben itt olvashatsz.

"A nagyon egyszerű implementáció pedig gy.k. ugyanaz, mintha kimockolnám."

Igen, kb. ugyanannyi munka, de egy csomó előnnyel jár, lásd a listát a kiinduló blog postban.

Kb. bárhova válaszolhatnék, szerintem - ide teszem. Két dolog, nagyvonalakban.

Egy:
Tegyük fel, van egy RESTlike API-m, amin felhasználók regisztrálhatnak. Feltétel, hogy a felhasználók 18+ évesek legyenek, s ezt a backend oldalon is szeretném ellenőrizni. Ha ez megvan, akkor hívok egy

dao.save(user);

-t. Ha nincs 18 éves, akkor exceptiont dobok, amit feljebb lekezelünk. A 18 éves-ellenőrzés egy másik interfészből jön, amit tesztelhetünk úgy is, ahogy neked szimpi, tesztelhetünk úgy is, ahogy nekem.
Ezt mockolva gyönyörűen tudom verify-olni, hogy tényleg elmentettük, ha el kell. Te milyen, egyszerű implementációt írnál erre, s hogyan ellenőriznéd?

Kettő:
Még mindig nem sikerült rendesen megindokolnod, hogy ez miben más, mint a mockolás.
Tulajdonképpen olyan implementációkat írsz, amik előre meghatározott bemenetekre, előre meghatározott választ adnak. Annyi a különbség eközött, meg a mockolás között, hogy:
* a mockokat tényleg tesztenként tudom másképp, másképp megírni
* a mockok sokkal, de sokkal kevesebb kódot jelentenek
* és, ami szerintem a legfontosabb, hogy az egyes unit tesztek teljesen függetlenek lesznek egymástól.

A te példádon még mindig azt látom, hogy:
* vagy külön-külön teszt implementációt írsz néhány (1-5) tesztenként
* vagy egy idő után olyan szinten összetett lesz a tesz implementációd, hogy képtelenség karbantartani.

És ezek mellett a teszt-kódtól messze van.

Ezek alapján az OP-ben említett érvek:
* modulárisabb: miért is? Az interfész.definícióm megvan, nem? Akkor miért kevésbé moduláris, ha mockot gyártok rá, mintha plusz egy implementációt?
* jobban újrafelhasználható: ez ugyanaz, mint az előző.
* egyszerűbb: lásd alább, a kevesebb kódnál.
* kevesebb kód: ez biztosan nem igaz, mert amíg mockkal két sorban megoldod egy metódos egyszerű implementálását (adott válaszra adott bemenet, mert te is csak ennyit csinálsz fent), addig a te módszereddel ez sokkal-sokkal több.
* kevesebb változó rész: miért is?
* Mock eltűnik: ez nem tudom, miért előny. (pont ennek az előnyeiről, hátrányairól vitázunk)
* Stub eltűnik: ez nem tudom, miért előny. (pont ennek az előnyeiről, hátrányairól vitázunk)
--
blogom

A fenti "Egy" példa se igazán példa. Mi ott a kód, amit tesztelnénk?
Írd le a kódot a tesztjével, majd én is leírom és összehasonlítjuk!

"Ezek alapján az OP-ben említett érvek: ..."

A teljes példák, amik szerepelnek a videóban vagy a másik, szöveges, Scala példás anyagban (To Kill a Mockingtest) , azok ezek ellenkezőjét mutatják.
Nézzük ez utóbbiakat:
Exhibit A: Fat dependencies
* modulárisabb: miért is? Nem függ a Config-tól
* jobban újrafelhasználható: Mivel nem függ a Configtól, ezért olyan helyre is beépíthető, ahol nem elérhető a Config.
* egyszerűbb: Nincs függősége a Config-tól, ezért annak ismeretének hiányában is megérthető.
* kevesebb kód: A kódra mondjuk azt, hogy kb. ugyanannyi, a teszt viszont öt soros volt eredetileg, most kettő. Ha még akarnád többféleképp is tesztelni, akkor még annyi szoros szorzó.
* Stub eltűnt: itt azért előny, mert egy hazugságra épült, azt hazudta, hogy a Config adja vissza azt a két értéket, pedig lehet, hogy a Config, nem is tudná ezt a két értéket visszaadni. Pl., ha az első érték csak páros lehet, a második meg nagyobb, mint 100.

Exhibit B: Mutable domain
* jobban újrafelhasználható:Mivel nincsenek változó részek (immutable, pure function-ök), ezért szabadon kombinálhatók.
* egyszerűbb:Mivel nincsenek változó részek (immutable, pure function-ök), ezért könnyebben megérthető.
* kevesebb kód:61 vs 39
* Stub eltűnt: szintén hazugságra épült, azt hazudta, hogy a Basketben levő áruk összértéke mennyi, miközben nincsenek is áruk a Basketben. Azt hazudja, hogy nincs túlköltés a kártyán. Ezek csak arra kellenek, hogy az implementációnak megfelelően az adott ágra menjen a program.
* Mock eltűnt: Nem az adott függvény tényleges működését teszteli, hanem azt, hogy az adott helyen az implementáció meghív-e egy függvényt adott paraméterekkel.

Ezt ugy kell erteni, hogy minden egyes kombinaciora kell gyartanom egy egesz osztalyt? A mockolasban ugye ez a szep, hogy a teszt lefuttatasa elott tetszolegesen felkonfiguralhatom a visszateresi ertekeket, milyen kivetel dobodjon stb. es meghivom a tesztet. Ebbol az egyszeru peldabol az jon le, ha azt akarom tesztelni, hogy mi tortenik, ha a hivas kivetellel szall el, akkor faraghatok egy ujabb implementaciot, ami azt dob.

-
Big Data trendek 2016

Többféle megoldás is lehet, egyrészt csinálhatsz több osztályt, konfigurálható osztályt (erre példa lehet a VendingMachine-es példa), vagy tovább refaktorálhatod a kódot, hogy az eredeti (production) osztályt használd.
Ez utóbbira példa a videóban a PotatoService használata.

"csinálhatsz több osztályt" ha van egy objektumom aminek 2 metodusat hivom egymas utan, es mindket metodus adhat vissza erteket, null-t, vagy dobhat kivetelt, akkor hany fake osztalyra is van szuksegem? maximum 9re.

"konfigurálható osztályt" arrol is lehetne egy kis eloadast tartani, hogy ez miert kerulendo. Az minden csak nem karban tarthato kod.

"tovább refaktorálhatod a kódot, hogy az eredeti (production) osztályt használd" B-t tovabb refaktoralom azert, hogy mikor A-t tesztelem, akkor B mukodeset szimulalni tudjam. A-nak semmi koze B-hez, annak implementalasahoz. Milyen hatassal van B tesztelhetosegere a refaktor? Mi tortenik, ha B harmadik fel altal irt kod?

Nem teged akarlak bantani (igazabol senkit sem) :) A mock meg spy technikakkal azt lehet merni/tesztelni, hogy adott metodus milyen side effecteket okozott. En nem fogom magam szivatni konfiguralhato osztalyokkal meg vegelathatatlan boiler plate kod irasaval. Nekem atlathatobb a
begin test
given
when
then
end test

Egy helyen latom, hogy mi mit ad vissza, nem kell bolyongani az osztalyok kozott, meg elnevezesi mintakat kovetni. A Mock keretrendszer meg oldja meg, ahogy akarja, hogy a fake/proxy/banomisen objektum a helyere keruljon.

-
Big Data trendek 2016

Nem tudom te/ti milyen rendszereket fejlesztetek. Mi egy HadoopAsAService megoldast reszelunk, ami 0-rol felhuz egy a felhasznalo altal konfiguralt Hadoop clustert 4 cloud szolgaltato valamelyiken. Felrantja az infrastrukturat, telepiti a szukseges komponenseket, bekonfiguralja es teszteli a clustert, metrikak alapjan fel es lefele skalaz, mindenzt igyekszik hibaturoen tenni (tech previewban ugyanezt tudja Marathon clusterekkel is). Hat hidd el van side effect boven (ezek csak a kulso tenyezok lecsupaszitva). Vagy ugy is mondhatnam, hogy csak side effect van mert olyan az uzleti logikank, hogy jol kotjuk ossze a kulonbozo side effecteket, taroljuk es kezeljuk a metaadatokat. El kepzelni sem tudom mennyi munka lenne mindent ezeknek az elveknek megfeleloen implementalni a production kod karbantarthatosaganak megorzese mellett. Annyi kombinacio, meg egymasra hatas van. Kenyelmes es egyszeru mockolni helyileg, es meg nem bizonyult szuk keresztmetszetnek. Az tud egy kicsit fajni, mikor mockolni kell a mockolt metodus visszateresi erteket. Ilyenre is van pelda, van egy tipus biztos (forditasi idoben kiderul, ha nem kompatibilis eventtet akar kuldeni egy flow) async flow managerunk, ami osszefogja a kulonbozo lepeseket egy folyamatta. Bizony nem ritkak a message.getResponse().getError().getReason() tipusu hivasok. Nem mondom, hogy nem lehetne valahogy mashogy megoldani, de ez igy mindenkinek kenyelmes (es ertheto), es megsem a teszt vezerli a production kodot.

-
Big Data trendek 2016

A használt lib-ek eleve meghatározzák, hogy mit egyszerű megcsinálni és mit nem, aztán van a csapat hozzáértése, a nyelvi lehetőségek, ...
Egyszerű kódot eleve nehéz elsőre írni, arra meg nem szokott idő lenni, hogy háromszor-négyszer refaktoráljunk, hiába lenne hosszú távon jó a fejlesztésnél, ha a rövid távú érdekek ezt felülírják. (Aztán meg nem értik, hogy egy félórásnak látszó módosítás miért is két nap)

Én itt inkább csak elvekről beszélek, nem a gyakorlatról.

"A tesztnél pedig készítesz hozzájuk egy nagyon egyszerű implementációt."

Az is stub-nak minosul:

"Stubs provide canned answers to calls made during the test, usually not responding at all to anything outside what's programmed in for the test. Stubs may also record information about calls, such as an email gateway stub that remembers the messages it 'sent', or maybe only how many messages it 'sent'."

Az nem valtoztat a dolgok allasan, hogy te magad implementalod es nem pl. mockito-val programozod fel a visszateritendo ertekeket. Ugyanugy megkapod a "rendes" stub-ok hatranyat, hogy a tenyleges, lecserelt ojjektum interfeszeben/hasznalataban torteno valtoztatast utana kell huzni a te "nagyon egyszeru implementaciodban". Sot, szerintem meg bonyolitja is a teszt kodot azzal, hogy a teszt szamara lenyeges parametereket (mint a visszateritett konstansokat) kulon fajlba teszed (feltetelezve, hogy az implementaciod kulon osztalyba kerul).

----------------------
"ONE OF THESE DAYS I'M GOING TO CUT YOU INTO LITTLE PIECES!!!$E$%#$#%^*^"
--> YouTube csatornám

Hát nem egészen ugyanaz, ("not responding at all to anything outside what's programmed in for the test"), ennek a nagyon egyszerű implementációnak mindenre kell reagálnia. Lásd a fenti példámat, ott nem csak az "1234" userId-re ad vissza igaz értéket, hanem minden másra hamisat. Az interface összes metódusát implementálni kell, így ha van egy authLocalUser metódusa is, akkor azt is implementálnia kell. Mocknál, ha authUser-re írtad meg a tesztet és a kód megváltozik authLocalUser használatra, akkor hibát fog jelezni a teszt, ráadásul nyomozni kell, hogy miért is.
Ez a nagyon egyszerű implementáció egy komplett, de nagyon egyszerű AuthService.

"Sot, szerintem meg bonyolitja is a teszt kodot azzal, hogy a teszt szamara lenyeges parametereket (mint a visszateritett konstansokat) kulon fajlba teszed (feltetelezve, hogy az implementaciod kulon osztalyba kerul)."

Ha ez nagyon kicsi és csak abban az egy teszt fájlban kell, akkor oda szokták tenni, ha nagyobb vagy több helyre is kell, akkor pedig paraméterezhetőre csinálják, pl. megkapja konstruktorban az elfogadott userId-t.

"Hát nem egészen ugyanaz, ("not responding at all to anything outside what's programmed in for the test"),"

Szamodra kedvezoen kihagytad a "usually"-t. :^)

"Lásd a fenti példámat, ott nem csak az "1234" userId-re ad vissza igaz értéket, hanem minden másra hamisat."

Mockito-s stub-ot is parameterezheted ugy, hogy barmilyen ertekre x-et teritsen vissza (sot, altalaban van egy teszt metodusod, ami teszteli, hogy AuthService a UserService parameteret kapja meg, itt egy mockot hasznalunk ami konkret erteket var, de a tobbi tesztesetben AuthService Stub-ot ugy parameterezzuk, hogy barmilyen ertekre true/false-ot teritsen vissza! - egy logikai assertion per teszt metodus elv). Mockitoval viszont megteheted, hogy csak azokat az ertekeket drotozod be, amelyek tenyleg fontosak a tesztnek. Ha a te implementaciodba kezdesz belepakolni mindenfele logikat (if donti el a visszateritett erteket), akkor tovabb bonyolitod a teszt kodot, mert egy bedrotozott ertek helyett, ami lenyeges az aktualis teszt szemszogebol, ertelmezned kell a feltetelt.

"Az interface összes metódusát implementálni kell, így ha van egy authLocalUser metódusa is, akkor azt is implementálnia kell. Mocknál, ha authUser-re írtad meg a tesztet és a kód megváltozik authLocalUser használatra, akkor hibát fog jelezni a teszt, ráadásul nyomozni kell, hogy miért is."

Es mi garantalja, hogy a te implementaciodban levo authLocalUser altal visszateritett ertek pont kompatibilis lesz a regi teszttel? Ha eleve ugyanazt a konstanst teriti vissza, akkor akar hibasan is atirhatod a prod kodot, hogy authLocalUser-t hasznaljon, de a teszt nem fog hibat jelezni. Mockito-val alapbol visszaterit egy null-t, lerohad a teszt, es szepen atirod, de legalabb kaptal egy emlekeztetot, hogy he, bizti, hogy ez igy jo lesz?

"akkor pedig paraméterezhetőre csinálják, pl. megkapja konstruktorban az elfogadott userId-t."

Ezzel elindulsz a Mockito ujraimplementalasanak az iranyaba, es megint elojon a bonyolodo teszt kod problemaja, mar van egy konstruktor parametered, bonyolultabb logika a sajat stub implementaciodban.

----------------------
"ONE OF THESE DAYS I'M GOING TO CUT YOU INTO LITTLE PIECES!!!$E$%#$#%^*^"
--> YouTube csatornám

"Szamodra kedvezoen kihagytad a "usually"-t. :^)"

Számomra az "általában nem" és a "mindig" az nagyon eltérő két fogalom, de matematikailag igazad van! :)

"Es mi garantalja, hogy a te implementaciodban levo authLocalUser altal visszateritett ertek pont kompatibilis lesz a regi teszttel? Ha eleve ugyanazt a konstanst teriti vissza, akkor akar hibasan is atirhatod a prod kodot, hogy authLocalUser-t hasznaljon, de a teszt nem fog hibat jelezni. Mockito-val alapbol visszaterit egy null-t, lerohad a teszt, es szepen atirod, de legalabb kaptal egy emlekeztetot, hogy he, bizti, hogy ez igy jo lesz?"

Ez van a Stub-nál, ahol ténylegesen nem is a működést teszteled, hanem az implementációt. Ha visszatérünk az előző pontra, hogy Stub-nál is mindenre reagál, akkor itt a Stub is ugyanúgy fog visszaadni valamilyen igaz/hamis értéket az authLocalUser-re. Illetve, ha logikailag nem ugyanúgy kell működnie az authUser-nek és az authLocalUser-nek, akkor a nagyon egyszerű implementációban is el kell térjen, pl. az egyik "1234"-et léptet be, a másik "local1234"-et.

Igen, viszont innentol kezdve n+1 AuthService implementaciot kell fejlesztened, hiszen fejlesztened kell a "normal" kodos service-ket plusz, a tesztben levot is, raadasul azt is ugyanugy karban kell tartanod. Hatrany: a teszt-ben levo AuthService-t a kutya nem fogja letesztelni semmire, hiszen a mocking ugyebar rossz, vagyis a jo isten se tudja, hogy a tesztben levo "egyszeru" implementacio mit ad vissza, illetve mikor adhat vissza rosszat, a coverage egy budos nulla lesz rajta. A tobbi teszt meg "best wishes" alapon hivkodja a TestAuthService-t, es baromira imadkozik, hogy a nyero kombinaciok jojjenek ki.

Egy stub annyibol jobb ennel, hogy a teszt elejen (anelkul hogy teljes mertekben implementalnod kellene az adott service-t) elore megmondhatod, hogy a teszt szempontjabol miknek kell tortennie.

Az eloado ott hibazott, hogy a stubbingnak egy rossz hasznalatat hozta fel. Ha nekem az authLocalUser-re kell tesztelnem, akkor nyilvan nem hasznalhatom az authUser-re felkeszitett stubokat, de nincs ertelmes ember a foldon, aki igy akarna hasznalni oket. Az, hogy a stubb elcsapja magat egy exceptionnel, hogy o bizony semmit nem tud az authLocalUser-rol, az egy teljesen hasznos failure, hiszen megmutatja azokat a pontokat, ahol a tesztet a valos kodhoz kell igazitani. Olyan, mintha egy NoSuchMethodException vagy egy NPE jonne fel.

A masik nagy hiba, hogy o osszemossa az unit test layert es az integration test layert. Unit testben nem az a lenyeg, hogy egy barmilyen ertelemben vett valos user be tud-e lepni a rendszerbe, ez akkor, ott a vilagon senkit nem erdekel. Az a lenyeg, hogy az az egyseg, amit epp tesztelunk, adott inputra adott outputot ad-e. Az integration teszt viszont mar nem hasznalhat mockokat, oda bizony - amennyire csak lehetseges - fel kell konfiguralni egy teszt rendszert, teszt adatokkal, es az UserService-nek az aktualisan tesztelt AuthService-t kell tesztelnie, nem csak valami zoknibabot.
--
Blog | @hron84
Üzemeltető macik

Csak kettőt kell karbantartani, a "normal"-t és a tesztet. A Stub-ok karbantartása és készítése se kevesebb munka.

"a coverage egy budos nulla lesz rajta"

Miért lenne 0? Hiszen eleve a tesztekben szerepel.

"a teszt-ben levo AuthService-t a kutya nem fogja letesztelni semmire, hiszen a mocking ugyebar rossz"

Ez egy szeparált modul, nagyon egyszerűen tesztelhető mocking nélkül.

"Egy stub annyibol jobb ennel, hogy a teszt elejen (anelkul hogy teljes mertekben implementalnod kellene az adott service-t) elore megmondhatod, hogy a teszt szempontjabol miknek kell tortennie."

Ez az amikor az implementáció mikéntjét teszteled.
Példa: Tfh. van egy CalcService modulod, amiben van egy funkció, ami két számot összead és logolja (adatbázisba)


int add(int a, int b)

A kódodban, amit szeretnél tesztelni van egy függvény, ami kiszámolja három szám összegét úgy, hogy használja a CalcSevice-t.


int add3(int a, int b, int c)

Pszeudó implementáció (a+b)+c:


int add3(int a, int b, int c) {
  int ab = CalcService.add(a, b)
  return CalcService.add(ab, c)

Hogyan teszteled ezt Stub-bal?
Szeretnéd ellenőrizni, hogy (2,3,5) paraméterekre 10-et ad-e. Felkonfigurálod a Stub-odat, hogy (2,3) paraméter esetén 5-öt adjon vissza, (5,5)-re meg 10-et.
Ha az implementáció megváltozik a+(b+c)-re, vagy (a+c)+b-re, akkor hibát fog jelezni a teszt.
Ha elrontják az implementációt pl. 2*(a+b)-re, akkor viszont azt mondja a teszt, hogy minden jó.
Illetve mi van akkor, ha nem egy inputra, hanem program által generált akár több száz inputra tesztelném? Ezt Stub-bal nem tudom megtenni.

"A masik nagy hiba, hogy o osszemossa az unit test layert es az integration test layert."

Nem Ő mossa össze, hanem jól tervezett rendszernél ez összemosódik, ami jó.
Lásd a fenti példámat, ha a CalcService-t úgy alakítják ki, hogy az add funkciója pure function, akkor nem kell szeparálni. Például, ha nem logol, hanem kiszámolja az összeget és visszaadja a logolandó információkat, akkor az pure function lesz és nem lesz neki mellékhatása, így nem kell szeparálni. Az add3 tesztelésénél maradhat a hívása, és tetszőleges sok inputra tudom tesztelni az outputot.


Pair<int, LogOut> add(int a, int b)

" hanem jól tervezett rendszernél ez összemosódik, ami jó."

Eddig harom ilyen "jol tervezett rendszerhez" is volt szerencsem, ahol a fejlesztok nem ismertek a kulonbseget az integration es unit teszt kozott, a tesztekre "junitok"-kent hivatkoztak. Egy nagyobbacska rendszernel ez oda vezet, hogy az osszes teszt futtatasa tobbtiz percig, vagy akar par oraig is eltart es senki sem futtatja oket commit/push elott, CI-s build failure emilekre meg szurot hoznak letre, mert "majd ranezek mikor lesz idom", de amig egy menedzser/team lead nem kezdi picsanrugni az embereket, napokig nincs sikeres build. Aztan amikor vegre valaki nekiall fixalni a build-et, a rengeteg integration teszt miatt, amik a hiba okatol valahol sokkal tavolabb fail-elnek, ez egy par oras moka lesz a jobbik esetben.

----------------------
"ONE OF THESE DAYS I'M GOING TO CUT YOU INTO LITTLE PIECES!!!$E$%#$#%^*^"
--> YouTube csatornám

Ezek valóban NEM "jól tervezett rendszerek" lehettek. Jól tervezett rendszernél másodpercekben mérhető az összes teszt lefutása unit tesztek esetén.

A burkolt véleményed mellet Te mit mondasz, egy függvényen belül minden külső hívást Stub-olni, Mockolni kell vagy csak a nem pure function hívásokat, vagy melyeket?

"Ezek valóban NEM "jól tervezett rendszerek" lehettek. Jól tervezett rendszernél másodpercekben mérhető az összes teszt lefutása unit tesztek esetén."

Unit teszteknel valoban, csak te ossze akartad mosni oket az integration tesztekkel, ezek meg sok esetben lemennek a fajlrendszerig/db-ig, ami miatt lassuak lesznek. Ha meg nincs kezdetektol leszogezve a ket kategoria kozotti kulonbseg es kikotve, hogy a tesztjeink nagyresze (>80%) unit teszt es csak keves integration tesztunk van, akkor a fejlesztok elkezdik felhalmozni az integration teszteket, uj/modositott kodot integration teszttel fogjak lefedni, nem unittal.

Egyelore csak a unit/integration tesztre vonatkozo kijelentesedre akartam reagalni, majd valamikor vegigolvasom az egesz szalat es valaszolok a masik bekezdesedre is, amugy is van par hozzafuznivalom az egyik fentebbi valaszodhoz. :^)

----------------------
"ONE OF THESE DAYS I'M GOING TO CUT YOU INTO LITTLE PIECES!!!$E$%#$#%^*^"
--> YouTube csatornám

"Egyelore csak a unit/integration tesztre vonatkozo kijelentesedre akartam reagalni, majd valamikor vegigolvasom az egesz szalat es valaszolok a masik bekezdesedre is"

Ez sajnos pedig csak együtt működik. Ha nem érted, hogy mit miért írok, akkor nyilván hibás következtetést vonsz le.

"Ha meg nincs kezdetektol leszogezve a ket kategoria kozotti kulonbseg "

Pont erre kérdeztem rá, hogy szerinted mi a különbség a két kategória között.
Illetve azt merem állítani, hogy van olyan unit teszt, ami egyben integrációs teszt (ezeknek soha nincs semmi mellékhatásuk, tehát nem mennek le fájlrendszerig/db-ig.), és minél nagyobb ezek aránya, annál jobban van tervezve a rendszer.

Ha te nem szigeteled el a tesztelt egyseg fuggosegeit, akkor elveszted a unit tesztek nagy elonyet, a "defect localization"-t. Peldaul legyen harom osztalyod, A, B es C. A metodusa meghivja B-t, az meg C-t. Ha te most irsz egy tesztet amely A-t black boxkent kezeli, akkor ha elbaszol valamit C-ben, ugyanaz a teszt hibat fog jelezni, de szamodra nem lesz egybol evidens, hogy hol van a hiba, melyik osztalyban, allhatsz neki debuggolni.

Ha viszont lecsereled A tesztjeiben B-t valamilyen test double-al, akkor tudod, hogy a failure oka csakis A-ban lehet. Sot, ha ertelmes nevet adsz a tesztednek es koveted az AAA patternt, egybol lejon, hogy hol van a kutya elasva.

Most jogos kerdes, hogy mik azok az egysegek, amiket erdemes lecserelni. Ha foo() metodusra irok tesztet, es foo() meghivja bar() es baz() privat metodusokat ugyanabban az osztalyban, akkor a nem fele hajlok, ha viszont athiv mas osztalyokba, akkor azokat szinte mindig lecserelem.

Szerk: ja es a masik nagy hatrany, ha nem mockolod ki B-t, hogy lehet, hogy tobb osztaly is hasznalja B-t, nem csak a, ha ezekre hasonlo teszteket irsz, akkor tobb, egymastol fuggetlen teszt is erinti B/C-t, ugyanazt tesztelik, ha elbaszol valamit C-ben, lesz tobb tiz hibat jelzo teszted, totalis kaosz. A masik meg, hogy lehet, hogy te A-ra irt tesztedben valoszinuleg nem teszteled B/C relevans reszeinek a mukodeset, hanem az alap visszateritett ertekre hagyatkozol. A kod coverage-ben meg B/C ezen reszei ugy fog tunni, hogy le vannak fedve, de valojaban nincsenek tesztelve.

----------------------
"ONE OF THESE DAYS I'M GOING TO CUT YOU INTO LITTLE PIECES!!!$E$%#$#%^*^"
--> YouTube csatornám

Tegnap felhoztam a temat a kollegaknak. Gondoltam kuldok nekik linket is, mert emlekeztem a topicra, ugyhogy most jol fel is tamasztom a szalat :)

> Ha te nem szigeteled el a tesztelt egyseg fuggosegeit, akkor elveszted a unit tesztek nagy elonyet, a "defect localization"-t

Erre a problemara nem csak egy megoldas letezik, es szerintem a masik eletkepesebb lehetne. Mar csak meg kell valositani :)

Mindent unitteszteljunk es szorjuk tele az egeszet mockokkal, amik miatt majd vagy nem tudunk refaktoralni, vagy a tesztjeink semmit se ernek, mert bele kell nyulni, mert a mock implementaciohoz kototte a tesztet helyett pl. defect localization-t ugy lehetne megoldani integration tesztek eseteben, hogy fel kell epiteni egy dependency fat (ahogy barmelyik IoC/DI framework is teszi), majd az integracios teszteket a levelektol elkezdve kell futtatni visszafele.

Mivel a levelek jo esellyel vagy pure functionok lesznek (ld alabb az idezetet*), ezert azokat trivialis unit tesztelni, vagy pedig I/O muveletet vegeznek, azokat meg ugyis mockolni/stubolni/test doubleni kell, mig az integracios tesztek futhatnak szintenkent visszafele.

Ezzel tobb legyet is utunk egy csapasra. Pure function-t unit tesztelni trivialis. Valami bemegy, mas meg kijon, ellenorizzuk es orulunk (igazi black-box teszt). Masreszt az integracios tesztek eseteben egyertelmu lenne az, hogy hol van a problema, mi az ami megdontotte a kartyavarat, es mar az alsobb szinteken akar fail fast modon, akar a reportingban kiemelve lehet latni azt a komponenst, amelyik eloszor volt erintve a hibaban. Onnan meg mar egyszeru a fix, nem kell orakig debugolni.

Mar csak annyi a problema, hogy implementalni kellene...

*: I'd take a slightly different take:
- Structure your code so it is mostly leaves.
- Unit test the leaves.
- Integration test the rest if needed.
I like this approach in part because making lots of leaves also adds to the "literate"-ness of the code. With lots of opportunities to name your primitives, the code is much closer to being self documenting.

https://news.ycombinator.com/item?id=15565875

Van egy harmadik út is. Ezt akkor még talán én sem tudtam, négy év alatt még én is fejlődtem. :-) Egyébként remélem, hogy ezt bármikor elmondhatom magamról, hogy a korábbiakhoz képest fejlődtem.

Szóval, a harmadik út.

Minden IO műveletet a lehető legkülső szinten kell elvégezni, alsóbb szinteken minden (kvázi) pure függvény.
A pure függvények unit tesztelése egyszerű, ráadásul lehet integráltan is tesztelni ezeket, ahogy korábban is írtam.
Az IO-t meg teljesen felesleges (unit) tesztelni, mert semmi pluszt nem ad.

Törekedni kell arra, hogy minden IO művelet a lehető legkülső, legmagasabb szinten menjen. Hogyan lehet ezt elérni? Nehezen! ;-) Jól kell tervezni a kódot: előbb elkérem az adatokat (IO), mindent, amit csak lehet; majd feldolgozgatom ezeket, amennyire csak lehet, ez a pure rész; majd kiírom, elküldöm a feldolgozott adatokat (IO).
Itt az IO-kat felesleges tesztelni, mert ami IO-t végezne, azt úgyis leválasztanám. Mivel itt csak IO van, ezért nem is fog semmi érdemleges futni.
- Mock/stub-bal beállítom mi kellene meghívódjon, mit kell visszaadnia,
- meg hívom azt, aminek meg kell hívódjon,
- majd ellenőrzöm, hogy az hív×odott-e meg, amit meghívtam,
- és ellenőrzöm, hogy azt adta-e vissza, amit az elején beállítottam.

Ennek aztán semmi teteje és értelme nincs!

Az egészet egyben sincs értelme ("unit") tesztelni, mert ha leválasztom az IO-t, az pontosan ua., mintha csak a pure részt magában tesztelném.
Az egészet esetleg lehet end-to-end tesztelni, de akkor meg nem akarom leválasztani az IO-t.

"Ez az amikor az implementáció mikéntjét teszteled"

Errol szol az unit teszt, kedves.

A kalkulatoros peldadra: attol fugg, _mit_ szeretnek tesztelni. Ha a CalcService mukodeset (vagyis, hogy 2,3 parameterekre 5-ot ad-e vissza) akkor nem stubolnam ki, hiszen ez a CalcService unit tesztje, azt nem stubolom ki. Ha ellenben a BigComputationalService -nek a unit tesztjet hivom, es a CalcService az nem egy ilyen kis konstansokat osszeadogato olcso muveletekkel operalo cucc, hanem valami DB-kereseses tortenet, akkor bizony kistubolom a CalcService-t, espedig azert, mert nem vagyok hajlando a BigComputationalService unit tesztje moge DB-t rakni, akkor sem, ha szeretned.

"Csak kettőt kell karbantartan"

Az pont eggyel tobb, mint amennyit karban szeretnek tartani.

Valamit felreertesz: a stub-ok nem kulonallo entitasok, hanem a tesztek szerves reszei, a teszt osztalyaiban szerepelnek, az egyes tesztek inicializaljak fel es szuntetik meg oket. Egy kulon TestAuthService azonban nem lesz a tesztek resze, az egy plusz egy implementacio lesz, aminek unit tesztje nemigen, vagy alig lesz, es ha hozzanyulsz a TestAuthService-hez ugy, hogy megvaltoztatod valamely "egyszeru implementacio"-nak a logikajat (mert mondjuk erre van szukseg egy uj tesztnel), akkor a valtozast vegig kell vezetned a regi teszteken is, akkor is, ha azok az epp fejlesztett funkcionalitast baromira nem erintik. Raadasul a jo isten a megmondhatoja, hogy hany hibat lehet ilyenkor veteni.

A stubokat pont emiatt talaltak ki, mert igy annyi implementaciod lehet, amennyit akarsz, viszont az eppen tesztelt funkcionalitason kivul nem kell azzal torodnod, hogy a tobbi fuggveny mit ad vissza, mert a kutya nem fogja meghivni oket. Azzal kell torodnod, hogy az eppen aktualisan fejlesztett cucc hivasai jok legyenek, fuggony.

Nem mondom, hogy minden esetben stubolni/mockolni kell, de ha valami nem szamokkal, hanem objektumokkal dolgozik, es fuggosege van barmilyen kulso eroforras fele (LDAP szerver, adatbazis, SOAP/RPC service, barmi), akkor erdemesebb inkabb azokat a reszeket kifake-lni, mintsem annyira szetszervezni az alkalmazas strukturajat, hogy ne kelljen stubolni/mockolni. Nagyon nem mindegy ugyanis, hogy 100 vagy 1000 osztaly kozott kell tudni navigalni, a kompaktsag egy eleg lenyeges szempont. Raadasul a szamologepnel ket fokkal bonyolult alkalmazasnal mar alapbol tobbszaz osztaly kozott kell tajekozodni, felesleges ezt a szamot exponencialis novesztesnek alavetni. Es igen, persze, dokumentacio... de azt is meg kell valakinek irni.

"jól tervezett rendszernél ez összemosódik, ami jó."

Te meg nem lattal jol tervezett rendszereket. Az integracios es unit teszteket ott lehet osszemosni, ahol az app mogott nincs komolyabb logika egy szobanoveny mukodesenel. Locsol, fold, nap, novekszik, kesz. Ahol van ket szerviz meg harom modell objektum, es az egesz app ot oldalbol/kepernyobol all, fixen odaszogezett ertekekkel, gyakorlatilag egy CRUD interface. Na az ilyeneknel ossze lehet mosni az unit meg integracios reteget, ugyanis az unit teszttel gyakorlatilag semmi mast nem tesztelnel, mint az egesz mogott levo frameworkot, ami nem a te scope-d. Amint _barmilyen_ uzleti logika is mogekerul az appnak (riportolas, integracio, jogkezeles, automatizmusok, stb, stb, stb), NEM megkerulheto az integracios tesztek levalasztasa a unit tesztetktol. Ez ketto ok miatt van igy:

1) az uzleti logika specifikacionak megfelelo mukodeset tesztelni kell. Igen, ez gyakorlatilag az implementacio teszteleset jelenti. Ha az LDAPAuthService nem hiv ki az LDAP-ba, csak otletszeruen ad egy true-t az a vilagon senkinek nem jo, legfeljebb neked, mert "hat nezz oda, true lett a vege, akkor vegulis minden happy, nem?" Hat nagyon nem.

2) Muszaj tesztelni a valos szervizekkel valo egyuttmukodest is, mert lehet barmilyen "egyszeru implementaciod", ha az a ruhes LDAP/SQL/SOAP nem az elkepzeleseid szerint mukodik, es olyat ad vissza, amit az "egyszeru implementacio" nem (mert mondjuk a kulso gyarto fejlesztett a kulso service-n a tudtod nelkul), akkor az alkalmazas jo esetben megmakkan, rossz esetben a komplett uzleti logika felborul. Nagyon nehez az ilyen eseteket kimagyarazni. Es ma a cloud service-k vilagaban (ahol a tenyleges mukodes akar percszinten is valtozhat) ez baromira nem egy elszabadult otlet.

Es csak hogy tisztazzuk: en sem stubolnek/mockolnek ki mindent. Ez - mint minden mas fejlesztoi eszkoz - olyan cucc, amit esszel kell hasznalni, es mindig, kivetel nelkul mindig merlegelni kell a hasznalatat, az adott esethez kell merni. De kiallni, es olyat mondani, hogy a mocking es a stubbing az ordogtol valo, es mindenestul kerulendo, na az az ostobasag melysotet kutjaba vetett pillantassal er fel.

Egyebkent imadom, amikor egyszeru kis peldakat hoztok fel, amit ranezesre se akarna senki stubolni. A valos eletben sosem kell CalcService-ket irni, hanem mindg viszonylag bonyolult esetek vannak, harom-negy-ot uzleti objektum is reszt vesz a dologban, de mindenki nagyvonaluan azt mondja, hogy "de hat azokkal ugyanigy kell eljarni". Vagy nem.
--
Blog | @hron84
Üzemeltető macik

"Errol szol az unit teszt, kedves."

Nem erről szól, rosszul tervezett rendszernél szól erről.
Arról kéne szólnia, hogy adott inputra, adott outputot ad-e a rendszer. A tesztelendő függvény egy fekete doboz, jó esetben előbb készül a teszt és utána az implementáció, nem ismerjük és nem is érdekes az implementáció mikéntje. Stub-nál és mock-nál pontosan tudod miket hív meg, nézed az implementációt és aszerint írod a tesztet.

"Valamit felreertesz:"

Nem értek félre semmit, sajnos Te nem akarod megérteni az egészet. :(

"Egyebkent imadom, amikor egyszeru kis peldakat hoztok fel."

Sajnos még az ilyen egyszerű példák kapcsán se sikerül megértetni a koncepciót. A példa videóban (VendingMachine) is három üzleti objektum van, a rossz kiindulásból átalakítja pure function-os megoldássá, ahol nem kell Stub, nem kell Mock, a unit tesztek egyben integrációs tesztek is. Az utolsó példában az email küldést választja le, én az adatbázis használat leválasztására mutattam példát. Ennyi példa szerintem elegendő, hogy ha valaki akarja és tudja is, akkor megérti a lényegét.

"A tesztelendő függvény egy fekete doboz"

Nem, unit teszt a kod kis egysegeit teszteli azokat elszigetelve a tobbi egysegektol. Nem kezelheted fekete dobozkent, mert akkor te integration teszteket irsz, nem unit teszteket.

----------------------
"ONE OF THESE DAYS I'M GOING TO CUT YOU INTO LITTLE PIECES!!!$E$%#$#%^*^"
--> YouTube csatornám

Ha pure function, akkor kezelhetem fekete dobozként. Tehát, ha csak az input-ot használja, valamint csak másik pure function-öket hív.

Példa:


def fun(a, b) = {
  c = a + b
  d = a - b
  return c * c + d * d

Itt, ha az összeadás és kivonás hívásokat nem választom le, akkor integrációs teszt vagy unit teszt?
És ennél?


def fun(a, b) = {
  c = add(a, b)
  d = add(a, b)
  return mul(c, c) + mul(d, d)
}

Mint lentebb irtam, nem kell abszolut minden egyseget kimockolni. Ilyen trivialis dolgokat meg, mint add/mul valoszinuleg akkor sem mockolnam, ha egy masik osztalyban lennekek statikus utility metodusok.

----------------------
"ONE OF THESE DAYS I'M GOING TO CUT YOU INTO LITTLE PIECES!!!$E$%#$#%^*^"
--> YouTube csatornám

"Ilyen trivialis dolgokat meg, mint add/mul valoszinuleg akkor sem mockolnam, ha egy masik osztalyban lennekek statikus utility metodusok."

Pontosan erről (is) szól a videó! Ilyen triviális dolgokból kell felépíteni a programot és akkor nem gond, ha az egyik függvény hívja a másikat (triviálst vagy pure-t), nem kell leválasztanom.

Az add/mul helyett bármi állhat, authentikációt ellenőrizhet, linkeket állít össze, mindenféle üzleti logikát csinálhat, amíg pure (nincs mellékhatása) addig nincs vele gond.
DE!, ha az add/mul hiába csak egyszerűen összead meg szoroz, ha mellette még adatbázisba is ír, vagy más mellékhatást csinál, akkor már azt sem hívhatom kedvemre, hanem le kell választanom.

A problema: nem tudsz mindent ilyen trivialis egysegekbol felepiteni, ugyanis ez reszben attol lesz trivialis, mert nagyjabol semmitol se fugg, szamok vannak benne, azt meg nem kell tesztelni, mert a JVM keretrendszer resze. Amikor viszont sajat objektumokkal kezdesz el dolgozni, az sosem lesz trivialis.
--
Blog | @hron84
Üzemeltető macik

Igen, a VendingMachine-esre, bocsi, elírtam!

"Eddig bármikor is vettem vmit automatából, sose lett új pénztárcám, és sose termett előttem egy másik automata."

Pontosan ezért is értik meg sokan nehezen, mert látszólag nem így történik, pedig valójában is így történik! :)
Csakhogy az vezeti félre az embereket, hogy közben a régi eltűnik, és az újról azt hiszik, hogy az a régi.
Pedig már Hérakleitosz is megmondta Kr. e. pár százban, hogy "nem léphetsz kétszer ugyanabba a folyóba.", mivel az ugye már egy másik folyó, a régi eltűnt.

Szerk.: A negatív számokat is ezért értik meg sokan nehezen. Talán így a legkönnyebb elmagyarázni: az, hogy -2-en utaznak a buszon az azt jelenti, hogy két embernek fel kell szállnia, hogy senki se legyen a buszon.

Mostanában mondták ezt neked? Engem valamiért kerülnek.

Forrás: Wikipédia
"Hérakleitosz talán legismertebb fennmaradt töredéke a folyó-hasonlat:

Hérakleitosz azt mondja valahol, hogy minden mozgásban van, és semmi sem marad változatlan, és a folyó áramlásához hasonlítva a létezőket, azt mondja, hogy nem léphetsz kétszer ugyanabba a folyóba.

Platón: Kratülosz 402 A"

Szerintem én jól értem, különben ugyanabba a folyóba lépnél, ami közben megváltozott.

Szerk.:


// 1. verzió (ugyanaz, csak változik):
river.change

// 2. verzió (új folyó)
river2 = river.change

A 2. példát így kellett volna írnom:


river = river.change

Így egy folyóm van, ami nem ugyanaz, mint a másik, közben megváltozott.
A régebbi folyót nem éred már el, így bele se tudsz lépni.

Ha el tudnád érni, akkor jönnének az alternatív jövők, pl. kinyitom a ládát és annyi a macskának, vagy nem nyitom ki, és él szépen tovább. Vannak olyanok akik szerint ennek a kvantum hülyeségnek is van értelme. Meg a foton ugye, most vagy itt van, vagy ott, de hogy adott valószínűséggel mindenhol, hát a csuda tudja!

"Így egy folyóm van, ami nem ugyanaz, mint a másik ..."

Itt tér el véleményed Platónétól.
A folyó-hasonlat tömör és frappáns, de mint sok hasonlóan tömör és frappáns dolog, így ez is többféleképpen (félre)értelmezhető, köszönhetően a nem kellő pontossággal definiált "ugyanabba" szónak. Bár a lábat körülvevő vízmolekulák mások, a Duna az Duna.

Filozofálni lehet, a gyakorlatban mégse rajzolják minden pillanatban újra a térképeket. Meg nekem se lesz másik pénztárcám, és az üzletnek se másik italautomatája.

A valóságban tényleg nem lesz új pénztárcád és nem terem elő egy másik automata, de ha teremne egy az előzővel teljesen megegyező és az előző eltűnne, akkor a végeredményt tekintve mi változna?
Semmi, hiszen azt hinnéd, hogy ez a régi pénztárca/automata, csak közben megváltozott.
Nem tudod felismerni, hogy nem ugyanaz, mert még a karc, amit belevéstél az is ott van.

Miért is jobb ez a modell nekünk, mint a régi?
Azért, mert így immutable objektumaink lesznek a mutable helyett.
Az immutable objektum azért jó, mert az csak egy érték, olyan, mint a 15, vagy true/false. Mindig annyi, sosem változik meg, nincsenek állapotai.

Miért rossz, ha van állapota?
Azért, mert bármivel kapcsolatba kerül azokat is "megrontja" (elbonyolítja). Ha egy függvénynél minél több ilyen van, annál nagyobb lesz az állapottér. Mit csinál ez a függvény? Attól függ, hogy ennek, ennek meg ennek éppen mi az állapota.

Nézzünk egy példát:


public class Number {
  private int a;
  public Number(int n) { a = n; }
  public void mul(int b) { a = a * b; }
  public int get() { return a; }
}

public class Service1 {
  public static void calc(Number x, Number y) {
    Service2.anotherCalc(x, y);
    x.mul(y.get() + 1);
  }
}

public class Service2 {
  public static void anotherCalc(Number x, Number y) {
    x.mul(y.get() + 1);
  }
}

Nagyon egyszerű a példa: egy objektum, egy állapot, mégis nagyon nehéz dolgunk van, hogy megmondjuk, hogy a Service1.calc metódusa mit is csinál. El kell mennünk a Service2-be meg kell értenünk az anotherCalc-ot, azt, hogyan változtatja az állapotteret, onnan továbbmenni a Number.mul-ba, mert az is állapotváltozást csinál. Minél több ilyen állapotunk van és minél hosszabb útvonalat kell végignézzünk, annál nehezebb dolgunk van.
A tesztelésnél le kellene szeparálnunk a Service2-őt és a Number-t, ezekhez jó Stubokat készíteni megint nem egyszerű.

Nézzük meg ugyanezt a példát leegyszerűsítve, immutable objektummal:


public class Number {
  private int a;
  public Number(int n) { a = n; }
  public Number mul(int b) { return new Number(a * b); }
  public int get() { return a; }
}

public class Service1 {
  public static Number calc(Number x, Number y) {
    Number result = Service2.anotherCalc(x, y);
    return result.mul(y.get() + 1);
  }
}

public class Service2 {
  public static Number anotherCalc(Number x, Number y) {
    return x.mul(y.get() + 1);
  }
}

Itt nincsenek állapotok, a Number az egy érték, emiatt könnyen érthetővé válik a Service2 és a Service1.
Mit csinál a Service2? x * (y + 1)
Mit csinál a Service1? Service2.anotherCalc(x,y)*(y+1).
Nem kell megértenünk a Service2.anotherCalc-ot, elegendő, ha tudjuk, hogy mit csinál, nem kell tudnunk, hogy hogyan csinálja, hiszen nem változtatja meg az állapotteret.
Mivel csak pure function-ök vannak, csak az inputtól függ az outputuk és nincs mellékhatás, így a Service1.calc-ba akár behelyettesíthetnénk a meghívott függvények belét, akkor is ekvivalens függvényt kapnánk, itt { return x*(y+1)*(y+1) }-et, emaitt nem kell leválasztanunk, Stub-olnunk!
Tesztelése minden függvénynek roppant egyszerű, csak meghívom a függvényeket adott inputokkal és ellenőrzöm, hogy a kívánt outputokat adják.

Ez mind szep es jo, de nem sikerult megertened a lenyeget. Az ebben a peldaban mutatott kiindulo kodnak nem az volt a hibaja, hogy stubokat/mockokat hasznaltak a tesztelesenel, hanem az, hogy szarul volt megirva. A mockoknak es a stuboknak itt semmi kozuk a vilagon semmihez, hajuknal kellene elorangatni, hogy problema legyen beloluk. Az igazi problema ezzel a koddal a szar szervezes.

Mondom maskepp: attol nem lesz kevesbe jo a teszt kod, hogy ott stubokat/mockokat hasznalok akar az elso, akar a masodik pelda eseteben. A pelda szempontjabol tokeletesen mindegy, hogy a Service2-t kistubolom, vagy nem stubolom ki, mert nem az a hiba, hanem az, hogy rosszul van a kod megirva.

Ugyanez volt a bajom a faszi altal hozott peldakra is. Tok jo, meg igaza van abban, hogy szarul volt megirva a kod, de nem azert volt szarul megirva, mert mockokat/stubokat hasznalt, hanem azert, mert szarul volt szervezve a kod. Rossz kovetkeztetes azt levonni nehany ilyen sikeres atszervezesbol, hogy a stubok/mockok rosszak es mindenestul kerulendok, mert minden esetben meg lehet kerulni a hasznalatukat pure functionokkel meg a tokom tudja meg mikkel. Nem mindig lehet.

Abbol, mert a Suzuki tipusu autoknak mindig elromlik az ajtonyito mechanikajuk, nem lehet azt a kovetkeztetest levonni, hogy ha egy kocsinak nehezen nyilik az ajtaja, az biztosan Suzuki. Valaki, valahol elteved az indoklasok surejeben.

Az van, amit mar eddig is mondtam: a mockok es a stubok jo eszkozok bizonyos feladatokhoz, de minden jo eszkozt lehet rossz celokra, vagy rosszul is hasznalni. Nem kell tudatosan kerulni a mocokok/stubok hasznalatat, jo baratsagban kell lenni veluk, de fel kell tudni ismerni, mikor kell okosan hasznalni oket. Ha igy lett volna megfogalmazva a video mondanivaloja, akkor egyet is ertenek vele. De hogy beallitja, hogy a mockok/stubok rosszak, az kb. olyan leegyszerusitese a problemanak, mint amikor azt mondjak, hogy a Java rossz, mert lassu.
--
Blog | @hron84
Üzemeltető macik

"Ez mind szep es jo, de nem sikerult megertened a lenyeget. Az ebben a peldaban mutatott kiindulo kodnak nem az volt a hibaja, hogy stubokat/mockokat hasznaltak a tesztelesenel, hanem az, hogy szarul volt megirva. A mockoknak es a stuboknak itt semmi kozuk a vilagon semmihez, hajuknal kellene elorangatni, hogy problema legyen beloluk. Az igazi problema ezzel a koddal a szar szervezes."

Sajnos Te nem akarod megérteni a lényeget! Nem az a kiinduló baj, hogy Stub-okat/Mock-okat használsz, hanem az, hogy rosszul tervezett a kód! Emiatt nem is tudod tesztelni anélkül, hogy Stub-okat/Mock-okat használnál.
Ha jól tervezett a kód, akkor nincs szükség a Mock-olásra. Ez a fő állítás, ezért kértem, hogy mutasson valaki, bárki egy olyan kódot és nem egy ködös leírást, ahol szerinte jól van tervezve a kód és mégis szükséges a Mock/Stub.

" a mockok es a stubok jo eszkozok bizonyos feladatokhoz, "

Pontosan! Tökéletesek a rosszul tervezett kódok teszteléséhez. Szerintem a videóban is ezt mondja.

Felvetettem egy problemat, te lerazod azzal, hogy egy kodos leiras. Ettol fuggetlenul erdekel, egy ilyen esetben, ahol egy fizetos API-val kell tesztelni, te hogyan oldanad meg a problemat? A Yahoo GeoCoding API leirasa nyilvanos, mutass egy "egyszeru implementaciot", ahol stub nelkul (figyelem: nem mocking/stubbing framework nelkul, hanem un. pure function-okkel) megoldod, hogy ez tesztelheto legyen!

Teljesen elkodositi az agyadat az, hogy osszemosod a ket problemat. A mockok es a stubok nem kizarolagosan a rosszul szervezett kodok tesztelesere jok, nagyon sok mas dologra is, csak neked egyszeruen tul keves koddal van tapasztalatod ahhoz, hogy ezt belasd. Felre ne ertsd, latatlanban elhiszem, hogy baromi jo fejleszto vagy, es N eve dolgozol egy nagyon elismert fejlesztocegnel ahol kivalo munkat vegzel - egy, legfeljebb nehany kodon. De otleted nincs arrol, hogy milyen allatkak rohangasznak a vadonban, te egy allatkertben eled az eleted, ahol meg az oroszlan is realtive biztonsagos.
--
Blog | @hron84
Üzemeltető macik

"De otleted nincs arrol, hogy milyen allatkak rohangasznak a vadonban, te egy allatkertben eled az eleted, ahol meg az oroszlan is realtive biztonsagos."

Sajnos én is a vadonban dolgozok, így tudom miről beszélsz. Nem hogy rosszul tervezett a kód, hanem sok esetben sehogy sem tervezett.
Ez nem jelenti azt, hogy én ne próbálnám legalább megérteni, hogy miért lesz nehezen tesztelhető, nehezen újrafelhasználható, nehezen érthető a kód és miért hemzseg a hibáktól. Ha már megértettem, akkor próbálom másoknak is ezt elmagyarázni, de, mint a fenti ábra is mutatja, sok fejlesztőt ez nem érdekel. Egy icipicit sem hajlandó képezni magát, vagy egy kis munkát belefektetni, hogy jobb kódot készítsen (tervezzen).

> Azért, mert így immutable objektumaink lesznek a mutable helyett.
> Az immutable objektum azért jó, mert az csak egy érték, olyan, mint a 15, vagy true/false. Mindig annyi, sosem változik meg, nincsenek állapotai.

Igy visszaolvasva azert erdekes, hogy ezt anno magyarazni kellett, mikozben az elmult 4 evben nem csak a devek, de a sysadminok/devopsok is immutable iranyba kezdtek menni, ld:

https://blog.codeship.com/immutable-infrastructure/

Nagy ceges kornyezetben mostansag az eldobhato infrastruktura, stateless microservice megy. Ahogy peldaul a HTTP protokol is stateless.

"A tesztelendő függvény egy fekete doboz"

Az az integracios teszt, kedves.

"jó esetben előbb készül a teszt és utána az implementáció,"

Ez igaz, ez a TDD, csakhogy unit teszt eseteben a konkret tesztelt osztalybol nem is szabad kistubolni semmit, legfeljebb mockkal lehet megmondani, hogy milyen hivasoknak kell megtortennie.

"Stub-nál és mock-nál pontosan tudod miket hív meg, nézed az implementációt és aszerint írod a tesztet."

Mert olyat stubolok ki, amit _mindenkeppen_ meg kell hivnia az implementacionak is. Segitek peldaval: az AuthService tesztjenek esetben ki fogom stubolni a UserService.getUser("userID")-t meg az AuthService kodjanak leirasa elott, hiszen tudom (le van irva a ruhes dokumentacioban!), hogy az usereket kizarolag az UserService.getUser() fuggvennyel lehet eloszedni, sehogyan mashogyan. Ha kezenallva gepelem be a kodot, akkor is igy kell tortennie. Vagyis en nem az implementaciot nezem, hanem a nyuves dokumentaciot, ahol le van irva, hogy minek kell tortennie.

Ezert mas az integration teszt mint a unit teszt. Ugyanis az unit test eseteben engem mocskosrohadtmodon nem erdekel akar az sem, hogy a UserService egy olyan interfesz, amit semmi nem implemental, hiszen nekem az AuthService tesztjeben abbol semmire nincs szuksegem, csak arra, hogy ha az AuthService meghivja a UserService.getUser-t, akkor ne NPE-t, ne null-t es ne NotImplementedException-t kapjon, hanem egy tetves User objektumot. Meg a User objektum tartalma is irrelevans! Az User objektum tartalma akkor lesz relevans, ha a ket service kozotti interoperabilitast tesztelem, ami az integration tesztnek a resze. Nem az unit teszte!

A faszikam peldai meg eroltetettek egytol-egyig. Olyan mockolos peldakat mutat, amiknel szetszervezes nelkul sincs szukseg a mockolasra, mert nincs relevans kulso fuggoseguk. Ami a VendingMachine-os peldat illeti, attol, mert lesz egy olyan service-d, ami nem fugg az adatbazistol, meg nem fogod lefedni azokat az eseteket, amikor a hiba van az adatbazislayerben, es a te service-d nem tudja elkezelni ezeket a problemakat, hiszen a teszt service baromira nem fog adatbazis exceptionoket dobalni. Ha osszemosod a unit es integration teszteket, akkor ezeket a dolgokat nem tudod ertelmesen letesztelni, hiszen valojaban te nem a kerdeses service-t hivogatod, hanem valami legbolkapott cuccot, aminek a jo mukodese a kutyat nem izgatja.

Az ilyen metodologiabol jonnek azok a problemak, hogy "de hat a tesztek alapjan ennek mukodni kell, a production-be miert nem megy?".

Eleg peldat adott, hogy megertsem a lenyeget: 1) legalabb harom dolog mosodik ossze a fejeben, 2) nem latott meg normalis mocking/stubbing real world example-t, es 3) csak olyan emberekkel kommunikalt eddig, akik nem ertettek meg a mocking/stubbing lenyeget.
--
Blog | @hron84
Üzemeltető macik

"Eleg peldat adott, hogy megertsem a lenyeget: 1) legalabb harom dolog mosodik ossze a fejeben, 2) nem latott meg normalis mocking/stubbing real world example-t, es 3) csak olyan emberekkel kommunikalt eddig, akik nem ertettek meg a mocking/stubbing lenyeget."

Ad három példát, ahol jól használja a Mockot, Stubot.
Áttervezi a kódokat olyanra, ahol nem kell a Mock, Stub, amitől modulárisabb lesz a kód, jobban újrafelhasználható, egyszerűbb, kevesebb a kód, kevesebb a változó rész.
Én ezek alapján nem azt szűröm le, amit Te.
Inkább ezeket:
1.) Az eredeti kód nem volt jól tervezve. (Ha valamiért nem lehet áttervezni a kódot, akkor valóban nem lehet mit tenni, szükség van és hasznos a mock/stub!)
2.) Az áttervezett kód jobban (jól) van tervezve.
1.), 2.) => Ha mock/stub kell a teszteléshez, akkor könnyen lehet, hogy rosszul tervezett a kódom és érdemes átterveznem.

nem mindegy, hogy hogyan nem volt jól tervezve. Hogy is mondjam érthetően... ha egy ház szénából meg nádból áll, és az első szél elfújja, annál nem nehéz jobbat mutatni.

Két probléma van:
- nagyon nem életszerű, hogy általában az emberek nádból meg szénából álló házakban laknának.
- ettól a sárból tapasztott ház nem lesz automatikusan jobb, csak a széna-nád háznál lesz jobb, de ebből nem lehet levonni az utolsó sorban említett következtetést.
--
Blog | @hron84
Üzemeltető macik

Harman haromfele esetet is mondtunk neked, amikor kellhet a mock/stub, en legalabb ketszer elmondtam, hogy kulso szervizhivasoknal altalaban szokott kelleni, de ugy latszik, akkor nem voltal jelen. Esetleg nem minden tizedik mondatot kellene elolvasni a kommentekbol. De legyen.

Peldaul van a Yahoo Geocoding API, fizetos, raadasul requestenkent szamlaz. A GeoCodingService-nek a unit tesztjeben peldaul nem engednem kihivni a fizetos API-ba, (mert baromi nagy luxus az ilyen), hanem a konkret kulso API objektumot kistubolnam, hogy azt a valaszt adja vissza, amire a tesztnek eppen szuksege van, anelkul, hogy akar egy(!) HTTP hivast is megengedne maganak. Es innentol kezdve akarmilyen pure functionokkel irod meg a GeoCodingService-t, kell a stub, mert kulonben fizethetsz, mint a katonatiszt. Tbh, a GeoCoding eseteben meg talan az integration teszt-nel is kistubolnam a GeoCoding API-t, mert nincs szukseg a valos funkcionalitasra, lenne egy darab teszt eset, ahol kihivok a valodi fizetos API-ba, es garantalom, hogy ez a teszt fenyevente egyszer fut le, a tobbi esetben pedig jo a kistubolt API is.

Es megegyszer: a fenti peldaban tokeletesen irrelevans, hogy hogyan van szervezve a kod. Ha pure functionokkel van megoldva, akkor sem lesz kevesbe fizetos a GeoCoding API.
--
Blog | @hron84
Üzemeltető macik

"Harman haromfele esetet is mondtunk neked, amikor kellhet a mock/stub, en legalabb ketszer elmondtam, hogy kulso szervizhivasoknal altalaban szokott kelleni, de ugy latszik, akkor nem voltal jelen. Esetleg nem minden tizedik mondatot kellene elolvasni a kommentekbol. De legyen."

Ha jól tervezett a kód, akkor nincs szükség a Mock-olásra. Ez a fő állítás, ezért kértem, hogy mutasson valaki, bárki egy olyan kódot és nem egy ködös leírást, ahol szerinte jól van tervezve a kód és mégis szükséges a Mock/Stub.

Ha direktbe hívod a service-t, akkor szükséges mock-olni! Viszont ez nem moduláris, nem újrafelhasználható, behozza a Mock/Stub-ot.
Nem moduláris, mert függ a direktbe hívott service-től.
Nem újrafelhasználható, mert ha nincs vagy nem használható az adott helyen az a service, akkor oda nem tudod berakni.
Behozza a Mock/Stub-ot: pl. "hazugságra" épül, az implementációt teszteled, nem a működést.

"Es megegyszer: a fenti peldaban tokeletesen irrelevans, hogy hogyan van szervezve a kod. Ha pure functionokkel van megoldva, akkor sem lesz kevesbe fizetos a GeoCoding API."

Nem irreleváns, mert ha interface-t hívsz, akkor a tesztben is könnyen le tudod cserélni, újrafelhasználható lesz, moduláris, ráadásul nem kell a Mock/Stub.

"Ha direktbe hívod a service-t, akkor szükséges mock-olni! Viszont ez nem moduláris, nem újrafelhasználható, behozza a Mock/Stub-ot.
Nem moduláris, mert függ a direktbe hívott service-től."

Hat mit kellene csinalnom, ha van egy fix szolgaltatasom, amit megiscsak hasznalni szeretnek? Mittudomen, legyen akkor valami egyszerubb, altalad is emesztheto dolog: ott van az MNB valutavalto szervize. Ez egy konkret szerviz, konkret peldakkal. Mutass nekem egy peldat arra, hogyan teszteled a szerviz hasznalatat! Szamokkal dolgozo szervizrol beszelunk, tehat meg csak nem is tulsagosan bonyolult, kb. mint a vending machine, csak itt a nyero szamokat nem a te kodod dobja.

Pont azert adtam kodos leirast, hogy lehetoseged legyen bizonyitani, implementacioval es tesztekkel, hogy neked van igazad. Adtam egy (igazabol ez mar a masodik...) valos eletbeli peldat, nem egy iskolai feladatot, oldd meg! Gondolom, ilyenekkel nap mint nap talalkozol a munkad soran, ha ennyire ertesz hozza, igazan nem lehet bonyolult megoldani egy ilyen egyszeru feladatot egy olyan hatalmas kodernek mint te vagy!
--
Blog | @hron84
Üzemeltető macik

"Hat mit kellene csinalnom, "

Leírtam már egy néhányszor. Interface-t kell csinálni és azt hívni.
Pl.:


double calcTotalPrice(IExchangeService exchangeService, Products products) {
  double sum = 0.0;
  for (Products product : products) {
    sum += exchangeService.exchange(product.price);
  }
  return sum;
}

interface IExchangeService {
  public double echange(double price);
}

class MNBExchangeService implements IExchangeService {
  public double echange(double price) {
    return MNBService.exchange(price);
  }
}

class TestExchangeService implements IExchangeService {
  private double rate;

  public TestExchangeService(double rate) {
    this.rate = rate;
  }

  public double exchange(double price) {
    return this.rate * price;
  }
}

// Test:
...
double rate = 15.0;
double productsPrice = products.calcTotalPrice();

double totalPrice = calcTotalPrice(new TestExchangeService(rate), products);
Assert.equals(totalPrice, rate * productsPrice);

"Pont azert adtam kodos leirast,"

A ködös leírás, az kb. egy specifikáció, amit meg lehet oldani jól tervezett kóddal is, és rosszul tervezett kóddal is, ezért kérnék kódot, ahol jól tervezett és mégis kell mockingolni.
Szerintem meg azért nem adsz ilyen kódot, mert nem tudsz ilyet adni (én sem tudok)!

Mutasd meg, hogyan teszteled a kodot arra az esetre, ha az MNB szervize eppen valami miatt nem elerheto.

Kulonben meg a TestExchangeService egy stub, csak ki van irva, hogy class. Pont ugyanazoktok a hibaktol szenved, mint egy stub, ha lenne egy alternateExchangeRate() hivas barmely tesztben, ugyanugy fogalma se lenne rola, hogy mit kezdjen vele. A kulonbseg csak annyi, hogy ez nem a teszt futtatasakor, hanem meg a forditasakor jonne elo, de ez ennek a stubnak a jellegebol adodik - de ettol ez meg egy stub, csak szegyenlosen nem igy hivjuk.

A valtozasokat igy is, ugy is vegig kell vezetni a stubokon/pure function implementaciokon, ez az, amit a faszikam nem mondott el. Nem a stubokkal van a baj, hanem a rossz fejlesztesi metodologiakkal.
--
Blog | @hron84
Üzemeltető macik

"Mutasd meg, hogyan teszteled a kodot arra az esetre, ha az MNB szervize eppen valami miatt nem elerheto."

Megmutattam, a teszt nem használja egyáltalán az MNB service-t. Ha nem elérhető, akkor is ugyanúgy fut.

Te is mutathatnál végre valamit.
Ha direktbe hívod a service-t, pl.:


double calcTotalPrice(Products products) {
  double sum = 0.0;
  for (Products product : products) {
    sum += MNBExchangeService.exchange(product.price);
  }
  return sum;
}

akkor ezt hogyan teszteled stub-bal pl. 4 product-ra?

"Kulonben meg a TestExchangeService egy stub, csak ki van irva, hogy class. ..."

Nem egészen. Ha megváltozik az implementáció egy teljesen szabályos refaktorálás miatt, akkor nem kell átírni, míg egy Stub-nál át kell, követni kell az implementációt.
Pl., ha a fenti erre változik:


double calcTotalPrice(Products products) {
  return MNBExchangeService.exchange(products.calcTotalPrice());
}

A tesztelés célpontja félremegy. Nem azt teszteled, hogy adott inputra adott outputot ad-e, hanem azt, hogy az implementáció lépései egymás után a kívántaknak megfelelően hívódnak-e meg.

Ha változik az API, akkor fordításkor jelez, és kényszerít, hogy implementáld. A Stub-nál nem jelez, nem kényszerít, esetlegesen hibásan fognak futni a tesztek: hibát jelez (ráadásul teljesen máshol), ha nincs is hiba, vagy nem jelez hibát, holott van hiba.

"Ha megváltozik az implementáció egy teljesen szabályos refaktorálás miatt, akkor nem kell átírni"

Nem feltetlen ertem, mire gondolsz teljesen szabvanyos refaktoralas alatt, gyakorlatilag ketto darab IExchangeService implementaciod van, es otletunk nincs, hogy az MNBExchangeService jol mukodik-e, azt teszteltuk, hogy tudunk-e olyan osztalyt irni, ami implementalja az IExchangeService interfeszt. Ez egy tok alap Java tudas tesztelesre hasznos, de hogy az MNBService mit csinal es mit nem csinal, azt senki sem tudja. Elkepzelheto persze, hogy az alkalmazas oldalan a TestExchangeService-t fogjuk hivogatni, de valljuk be, ez a ritkabbik eset.

Tegyuk fel, hogy az alabbi modon irom at az MNBExchangeService-t:


abstract class BasicExchangeService implements  IExchangeService {
  public abstract double echange(double price);
}
class MNBExchangeService extends BasicExchangeService {

}

A teszt sikerrel le fog futni, az app pedig hibas lesz. Hogy miert? Mert senki nem teszteli a valos mukodest, senkit nem erdekel, hogy az app egyebkent mukodik-e.

De ez csak a megoldasod egyik problemaja.

A masik, hogy tegyuk fel, az alkalmazasba beleinjektalod a TestExchangeService peldanyt (hogy most az app inicializaciojanak konstruktorban adod at, vagy DI-vel benyomod, az irrelevans), miben lesz ez jobb, mint egy stub? A TestExchangeService-t ugyanugy at kell irni, ha valtozik 1) az interfesz definicioja 2) a kalkulacios logika (peldaul ha a valtas soran hozza kell adni egy fix atvaltasi koltseget), vagyis valojaban ket ExchangeService-t kell karbantartani, ebbol az egyiket csak azert tartjuk karban, mert vicces, ha ket ExchangeService-nk van egy helyett, ugyanis az egyiket senki elo ember nem hasznalja, az masikat pedig senki elo ember nem teszteli. Soha.

Na tipikusan ezek azok a helyzetek, amikor a modularizacio nem ekvivalens a jol szervezettseggel. Nem eleg csak arra verni, hogy nekem modularis az alkalmazasom, azt is meg kell indokolni, hogy ez miert jo. Jelen esetben ez nem jo, es nem is latom az indokat, hogy az ExchangeService-t ennyire szetbonyolitsam, foleg ugy, hogy ezzel el fogom vesziteni azt, hogy az MNBService barmilyen modon tesztelheto legyen.
--
Blog | @hron84
Üzemeltető macik

"Ha megváltozik az implementáció egy teljesen szabályos refaktorálás miatt, akkor nem kell átírni"

Nem feltetlen ertem, mire gondolsz teljesen szabvanyos refaktoralas alatt, gyakorlatilag ketto darab IExchangeService implementaciod van, es otletunk nincs, hogy az MNBExchangeService jol mukodik-e, azt teszteltuk, hogy tudunk-e olyan osztalyt irni, ami implementalja az IExchangeService interfeszt. Ez egy tok alap Java tudas tesztelesre hasznos, de hogy az MNBService mit csinal es mit nem csinal, azt senki sem tudja. Elkepzelheto persze, hogy az alkalmazas oldalan a TestExchangeService-t fogjuk hivogatni, de valljuk be, ez a ritkabbik eset.

Tegyuk fel, hogy az alabbi modon irom at az MNBExchangeService-t:


abstract class BasicExchangeService implements  IExchangeService {
  public abstract double echange(double price);
}
class MNBExchangeService extends BasicExchangeService {

}

A teszt sikerrel le fog futni, az app pedig hibas lesz. Hogy miert? Mert senki nem teszteli a valos mukodest, senkit nem erdekel, hogy az app egyebkent mukodik-e.

De ez csak a megoldasod egyik problemaja.

A masik, hogy tegyuk fel, az alkalmazasba beleinjektalod a TestExchangeService peldanyt (hogy most az app inicializaciojanak konstruktorban adod at, vagy DI-vel benyomod, az irrelevans), miben lesz ez jobb, mint egy stub? A TestExchangeService-t ugyanugy at kell irni, ha valtozik 1) az interfesz definicioja 2) a kalkulacios logika (peldaul ha a valtas soran hozza kell adni egy fix atvaltasi koltseget), vagyis valojaban ket ExchangeService-t kell karbantartani, ebbol az egyiket csak azert tartjuk karban, mert vicces, ha ket ExchangeService-nk van egy helyett, ugyanis az egyiket senki elo ember nem hasznalja, az masikat pedig senki elo ember nem teszteli. Soha.

Na tipikusan ezek azok a helyzetek, amikor a modularizacio nem ekvivalens a jol szervezettseggel. Nem eleg csak arra verni, hogy nekem modularis az alkalmazasom, azt is meg kell indokolni, hogy ez miert jo. Jelen esetben ez nem jo, es nem is latom az indokat, hogy az ExchangeService-t ennyire szetbonyolitsam, foleg ugy, hogy ezzel el fogom vesziteni azt, hogy az MNBService barmilyen modon tesztelheto legyen.

Itt a helyes megoldas az, hogy ugyan szetmodularizalom (ez eddig jo irany!), de az MNBExchangeServiceTest-ben kistubolom az MNB altal biztositott API egyes osztalyait, es megnezem, hogy maga az MNBExchangeService egyaltalan mukodokepes-e, ugyanis _ez_ az, ami az alkalamzas szempontjabol fontos, nem pedig az, hogy a TestExchangeService tud-e barmit csinalni.

Tbh egyebkent ha nem varhato egyszerre tobb Exchange szolgaltato megjelenese a kodban, nem is erdemes szetmodularizalni, mert ez csak felelsegesen bonyolitja a projektet, egy egyszeru refaktoralassal kicserelheto az osztaly neve, ha erre barmikor szukseg mutatkozik.

Az app tobbi resze szempontjabol pedig tokeletesen irrelevans, hogy egy valodi stubot injektalok be az alkalmazasba, vagy egy ilyen moricka-implementaciot, a stubnak csak annyi elonye van, hogy azt nem kell kulon osztalykent megirni es karbantartani, eleg csak az adott teszt igenyeihez felkesziteni.

Nem szabad elvesziteni szem elol azt, hogy miert irjuk a teszteket: hogy az alkalmazashibakat meg fejlesztesi idoben megfogjuk. Nem azert, mert fapolni akarunk arra, hogy nekunk vannak tesztjeink.
--
Blog | @hron84
Üzemeltető macik

"Te is mutathatnál végre valamit.
Ha direktbe hívod a service-t, pl.:


double calcTotalPrice(Products products) {
  double sum = 0.0;
  for (Products product : products) {
    sum += MNBExchangeService.exchange(product.price);
  }
  return sum;
}

akkor ezt hogyan teszteled stub-bal pl. 4 product-ra?"

Az erre adandó példa kimaradt, erre is válaszolnál?

"A teszt sikerrel le fog futni, az app pedig hibas lesz. Hogy miert? Mert senki nem teszteli a valos mukodest, senkit nem erdekel, hogy az app egyebkent mukodik-e."

Eddig a unit tesztekről beszéltünk. Stub esetén se a valós működést teszteled, hiszen a valós service hívást kistubolod. Természetesen mindkét esetben tudod a valós működést tesztelni, nem stubolsz, vagy az MNBExchangeService-t adod át. Ez viszont már rendszerteszt, aminek nincs helye a unit tesztek között.

A többi részre már több soron is válaszoltam.

Je, ugy tudtam, nalad osszemosodik a unit es az integration teszt fogalma, pont azert probalom en is egyszerre kezelni oket (bar megmondom oszinten, egy kicsit nehez), mert azt mondtad, egy jo alkalmazasnal ossze lehet oket mosni. Most akkor megiscsak kulon kellene kezelnem az unit teszteket az integration tesztektol, vagy hogy van ez?

Ami a 4 product-os peldadat illeti nagyon egyszeru (most keltem, nem vagyok hajlando utananezni a konkret kodnak, de gondolom ennyibol is megerted): egyszeruen kistubolom az MNBService.exchange() (figyelem! NEM az MNBExchangeService.exchange()-t!) fuggveny hivasat a teszt setup() -jaban ugy, hogy egy fix rate-tal szorozza fel az inputkent kapott arat. Hogy ez miben jobb, mint a te implementaciod? Technikalilag semmiben, csupan annyiban, hogy nem kell egy plusz osztalyt karbantartanom, illetve ugyanaz az osztaly (MNBExchangeService) kerul felhasznalasra, mint ami az alkalmazasban, vagyis ha a MNBExchangeService.exchange() fuggvenyben valami strukturalis baki van, az ki fog bukni, az MNBService.exchange() meg ugysem az en kodombol jon, azt nem nekem kell lefedni tesztekkel.

Megegyszer, lassan leirom, hogy megertsd: nem a modularizacioval van elsosorban a gond, az jo dolog. A rossz dolog az az, ha nem teszteljuk a konkret implementaciot arra, hogy jol mukodik-e. Az MNBExchangeService eseteben igenis az elvart mukodes resze az, hogy belehivjon az MNBService-be, es ha ezt nem teszi, elvarom a teszttol, hogy hibat dobjon, mert engem nem az erdekel, hogy random szamokat vissza tud-e adni, hanem az, hogy az MNBExchangeService tenyleg az MNB megfelelo webszervizet hivogatja, es nem a holnapi lottonyeroszamokat sorolja vissza valaszkent. Az alkalmazas szempontjabol ugyanis nem elegseges az, hogy az MNBExchangeService.exchange() szamok beadasara szamokkal valaszol-e, hanem az is fontos tenyezo, hogy ezek a szamok hogy allnak elo. Mivel a kulso gyarto service-be gyakran koltseges belehivni (akar financialisan akar a futtatas idokoltsegevel szamolva), ezert a unit teszt eseteben a gyarto altal biztositott API feluletet ki lehet mockolni es stubolni, mert nem szempont az, hogy tenyleg kihivjunk az MNB szervizebe, az a szempont, hogy az implementacio megtenne-e.

"Stub esetén se a valós működést teszteled, hiszen a valós service hívást kistubolod."

Itt azonban nem az MNBExchangeService kistubolasarol beszelunk, hanem a gyarto altal adott MNBService kistubolasarol, ami adott esetben nem para, ugyanis az MNBExchangeService-tol a unit teszt szintjen csak az az elvaras, hogy az MNBService-t hasznalja a szamok eloallitasa soran, es hogy a megfelelo atalakitasokat vegezze, de nem elvaras, hogy a mindenkor aktualis USD-HUF arfolyam mentsen valtson, az mar a rendszer/integration teszt feladata.

Tehat, amire itt probalok rautalni, es amit te nagyvonaluan kikerultel a sajat peldadban, az az MNBExchangeService unit tesztje, amit nem lehet kihagyni, hiszen ez az, amivel az alkalmazas valojaban dolgozik. Ezt az osztalyt nem eleg csak a rendszer/integracios tesztek szintjen lefedni, mert az adott esetben pontatlansagokhoz is vezethet, illetoleg fontos szempont az, hogy ezek a tesztek sokkal ritkabban futnak, mint a unit tesztek, vagyis nagyobb az eselye, hogy sokaig hibas koddal zorog a szeker.

Es meg egy fontos dolog: azert is erdemes minel tobb tesztben, minel tobb kornyezetben hasznalni az MNBExchangeService-t, mert ha nem ilyen egyszeru az implementacioja, hanem tobb allapottol is fugg az uzleti logika, ami hajtja, akkor jobban ki tud bukni, hogy hiba van, attol fuggetlenul, hogy ez egy masik teszt soran bukott ki. Ugyanis az, hogy ha egy teszt soran kibukik, hogy egy, a teszttol egyebkent tobbe-kevesbe fuggetlen ponton hiba van, az mindig jo, ugyanis a teszt olcso, de ha ugyanez a productionben jon ki, az eleg draga tud lenni mindenki eleteben. Raadasul a tesztek kodja mindig egyszerubb a vegleges alkalmazas kodjanal, azt debugolni is sokkal konnyebb (meg a mockokkal/stubokkal egyutt is), mintha az aktualis kodot kellene debugolgatni.
--
Blog | @hron84
Üzemeltető macik

"Je, ugy tudtam, nalad osszemosodik a unit es az integration teszt fogalma, pont azert probalom en is egyszerre kezelni oket (bar megmondom oszinten, egy kicsit nehez), mert azt mondtad, egy jo alkalmazasnal ossze lehet oket mosni. Most akkor megiscsak kulon kellene kezelnem az unit teszteket az integration tesztektol, vagy hogy van ez?"

Megpróbálom kicsit részletesebben kifejteni.

- Unit tesztnél a programnak egy izolált kis egységét (unit) (általában függvény vagy osztály) akarod egyszerre tesztelni.
- Az integrációs tesztnél pedig a kis unit-ok együttműködését akarod tesztelni.

Azért mosódik össze ez a kettő, mert nincs értelme szigorúan izolálni az egységet.

Vegyük pl. ezt a kódot:


public String calc(int a, int b) {
  StringBuilder sb = new StringBuilder();
  int result = Service.operation(a, b);
  sb.append("a op b =")
    .append(a).append(" op b =")
    .append(a).append(" op ").append(b).append(" = ")
    .append(result);
  return sb.toString();
}

Itt a calc-ot akarjuk unit tesztelni. Ha szigorúan izolálni akarnám, akkor a StringBuilder függvény hívásait és az Service.operation hívást is izolálni kellene, gyakorlatilag minden sort. Ennek nem igazán van értelme. Ha viszont a StringBuildert nem izolálom, akkor a calc unit tesztje egyúttal a StringBuilderrel való integrációs tesztje is lesz.
Hasonlóan, ha a Service-t nem izolálom, akkor a Service-szel való integrációs tesztem is lesz.
A videóban és a leírásban Ken Scambler azt mondja, hogy ha az input és az output determinisztikus, akkor felesleges izolálni. Ha nincs izolálva, akkor viszont integrációs teszt is lesz. Ha megnézed ilyen szemüvegen át a unit tesztjeidet, akkor meglátod, hogy a túlnyomó többség egyben integrációs teszt is.
Ha viszont az input vagy az output nem determinisztikus, akkor mindenképp szükséges izolálni!
Ha külső service hívás van, akkor annak az eredménye nem determinisztikus (pl. nincs net, nem ad eredményt), ezért izolálni kell.
Minél kevésbé kell izolálnod a unitodat, annál hasznosabban fog lefutni a "unit" tesztje, hiszen annál inkább integrációs teszt is. Elvileg az a legjobb, ha a unit teszt teljesen integrációs teszt is egyben.
Ha külső service hívás van, akkor mindenképp lesz olyan integrációs teszted is, ahol a unit-ot a külső service integrálódásával teszteled, de az már nem a unit tesztek közé kerül. Csak ez utóbbiakat szokták sokan integrációs tesztnek hívni. Bár ahogy az előbb leírtam ez a megnevezés nem fedi le a valóságot, ahogy a unit tesztek sem csak unit-okat tesztelnek, hanem integrálódást is.

"StringBuilder függvény hívásait és az Service.operation hívást is izolálni kellene, gyakorlatilag minden sort."

Nem, mert az mar out of the scope. Amit itt izolalni kell, az a Service hivasa, de azt is csak akkor, ha a te kododban van, ha egy kulso libbol jon, azt megint nem a te feladatod tesztekkel lefedni. A StringBuilder hivasait meg aztan vegkepp nem, jo hogy a bytekod-feldolgozast nem akarod tesztekkel lefedni.

Egyebkent a unit tesztjeim jo resze nem integracios teszt pont azert, mert mindegyik Service.operation-jellegu hivast kistubolom, hogy ne bukjon el a calc tesztje azon, ha a Service (barmi is legyen mogotte) implementacioja akar meg meg sincsen, vagy csak interfesz szintjen van meg (TDD, ugyebar). Aztan van egy integracios teszt is, ahol kb. ugyanez a hivas lefut, de stub nelkul.

Unit teszt eseteben csak azt nem stubolom ki, ami nem az en kodomban van, es/vagy nincs kulso szolgaltatasra fuggese (DB-re, LDAP-ra, webservice-re, barmi egyeb masra). Nalam ez az okolszabaly, aztan hogy ez kinek tetszik es kinek nem, az mas teszta.

Es pont azert nem is akarom osszemosni az unit es az integracios teszteket, mert ha hibat keresek, akkor reszben az elfailedezo tesztek alapjan hatarolom korbe a hiba okat es jelleget. Ha minden egysegem teszje tobb kulonbozo egysegtol fuggne, akkor konnyen elkepzelheto olyan allapot, hogy az alkalmazas elindul meg fut, megis a tesztek tobb mint fele kihal, mert pl. az Amazon epp nem elerheto.
--
Blog | @hron84
Üzemeltető macik

Nem. Ha jol ertem, az UserService-nek egy feladata van: usereket kell visszaadnia kulonfele feltetelek alapjan. Kesz, ennyi, nem tobb, nem kevesebb. Neki egy fuggosege van: az UserRepo, ez az, amibol eloszedi a usereket adott feltetelek mellett (a feltetelek azok, amiket csak a service tud). Az AuthService feladata a userek authentikacioja, majd az UserService-bol a megfelelo user lekerdezese, es visszaadasa. Az UserService-nek elvben meg tudnia sem kell az AuthService letezeserol, hiszen az o mukodese pontosan nulla mertekben fugg az AuthService letezesetol vagy nem letezesetol. Valami eleg elborult elme kell ahhoz, hogy en a UserService-nek parameterkent (!!!) atadjam az AuthService-t.
--
Blog | @hron84
Üzemeltető macik

"Az AuthService feladata a userek authentikacioja, majd az UserService-bol a megfelelo user lekerdezese, es visszaadasa. "

Azt hiszem nem nagyon nézted figyelmesen a videót. Abban a példában a UserService-nek az AuthService és a UserRepo a függősége. A UserService-nek van egy getUser függvénye, ami először authentikálja a user-t az AuthService segítségével, majd lekéri az adatait a UserRepo-ból.

Munka mellett neztem, nem tudtam 1000%-ban figyelni. Ez igy viszont strukturalis baki, mert pont forditva kellene mukodnie szerintem, a UserService-nek nem szabadna, hogy tudjon arrol, hogy a user authentikaltatni is kell, a getUser a too many responsibility tipikus peldaja ez esetben. Semmi gond a peldaval, csak a torka veres, de nem azert, mert a tesztek szarul vannak csinalva, hanem ugy strukturalisan az egesz van elbokve.

Disclaimer: ez esetben a fenti indoklasomat ugy olvasd, hogy en alapbol forditott felallast felteteleztem, mert ugy volt logikus.
--
Blog | @hron84
Üzemeltető macik