Tesztesetek írásakor a rendszerben lévő metódusokat hívjuk, és ellenőrizzük, hogy a kívánt eredményt kapjuk-e. Tipikusan assert-et és ennek variációit használjuk, hogy teszteljük, hogy az általunk elvárt eredmény megegyezik-e a tényleges eredménnyel.
Nagyon egyszerű, de nem túl realisztikus példa:
A teszt kód pedig:
Hogyan működik egy assert?
Ha az assert után megfogalmazott logikai kifejezés hamis eredményt ad, a környezet exception-t dob. Ez tipikusan a tesztkód futásának sikertelenségét eredményezi, amelyet a fejlesztői környezet rendszerint egy piros bucival jutalmaz. (Ha lefut a teszt, akkor a teszthez tartozó buci zöld lesz.)
Alaphelyzetben Java forráskód fordításánál az assert-eket be kell kapcsolni a -ea kapcsolóval. (Microsoft platformon is alaphelyzetben csak debug fordítói üzemmódban “élnek”.) Ha nincsenek bekapcsolva az assert-ek, akkor platformtól/nyelvtől függően vagy
- eleve nem kerülnek bele a lefordított kódba ezek az utasítások (pl. C, C++), vagy
- a futtató környezet (pl. JVM) átlépi őket.
Assert-ek a produkciós kódban
Az assert-eket nem csupán a tesztelést végző kódban használhatjuk, hanem “elszórhatjuk” őket a produkciós kódban is. Ennek célja a futás közbeni elő- és utófeltételek ellenőrzése, és az invariánsok kifejezésre juttatása, és ezek ezáltal a program biztonságosabbá tétele. (Az invariánsok olyan kifejezések, amelyeknek az értéke a program végrehajtása közben ugyanaz.)
Példák:
- (előfeltétel) van egy Book osztályom, amely rendelkezik egy sell metódussal (ez akkor hívódik, ha a könyvet eladják). Egy könyvet nem lehet többször eladni, és ezért normál esetben a sell metódus sem hívható meg egynél többször (mert a GUI biztosítja, hogy ne lehessen ráklikkelni egy már eladott könyvre). Ha biztosak akarunk lenni benne, hogy egy eladott könyvre nem hívják meg többet a sell metódust, kezdjük így a metódust: assert !sold; (A sold boolean mező jelzi, hogy a könyvet eladták-e.)
- (invariáns) van egy egészekből álló listám, amelyben az elemek összege mindig 100. A listába magam teszek be elemeket, és veszek is ki belőle, de szeretnék biztos lenni abban, hogy soha nem tévedek, és az összeg valóban 100. Ezt új elem hozzáadásakor vagy meglévő törlésekor egy megfelelő assert-tel biztosíthatom.
Sokan idegenkednek az assert-ek használatától: nem értenek egyet azzal, hogy a tesztkódon kívül is használatban legyenek, ill. ha használatban is vannak, a produkciós környezetben mindenképpen legyenek kikapcsolva. Ezt a következő érvéléssel támasztják alá:
- Nem valók a tesztkódon kívüli kódba, és beszennyezik azt.
- Az én kódom hibamentes, és ezt a unit (vagy más) tesztekkel igazolom.
- Az assert-ek csökkentik a produkciós kód futási sebességét.
Az első érvvel igazából nem tudok mit kezdeni. Sajnos, a Java, C# és nagyon sok más nyelv sem ad lehetőséget arra, hogy peremfeltételeket fogalmazzunk meg az egyes metódusok, függvények alkalmazhatóságát illetően.
A második nem igaz: hiába is lenne a unit tesztek általi kódlefedettség 100%-os, ez egyáltalán nem garancia a hibamentességre (erről külön post-ban igyekszem majd írni). A harmadik állítás pedig természetesen igaz, azonban az esetek többségében a lassulás nem észrevehető.
Az én érvem az assert-ek tesztkódon kívüli használatára az, hogy ezáltal előírhatjuk azokat az elő- ill. utófeltételeket, ill. invariánsokat, amelyeket az egyes metódusok futásakor elvárunk, és ennek hatására csökkentjük az adatkorrupció lehetőségét, ugyanis ha a produkciós környezetben a feltétel nem teljesül (tehát olyan állapotba kerül a program, ami a program írásakor szerintünk nem volt elképzelhető), a program exception-t generál, és alapesetben a tranzakciók visszagörgetődnek. A rendszer visszaáll egy (remélhetőleg) stabil állapotba.
Ha nincsenek ilyen biztonsági mechanizmusok beépítve a programunkba, akkor könnyen szembesülhetünk azzal, hogy az adatbázisban tárolt adatok egy része értelmezhetetlen lesz, és a program futása is “eltéved” (a felhasználók megmagyarázhatatlannak tűnő hibabejelentéseket produkálnak). Sokszor fordul elő, hogy a hiba csak később derül ki. Ráadásul a program alkalmazása során a hiba hatása tovább gyűrűzik, és végül még több adatot szennyez be. Ilyenkor adattisztító programokat/szkripteket írhatunk, hogy helyre tegyük az adatbázist (ha tudjuk egyáltalán, hogy mi az, ami elromlott), és persze az eredeti hibát is ki kell javítanunk, hogy ne fordulhasson elő még egyszer. A programhiba megtalálása az eltorzult, korrupt adatok alapján még nehezebb.
Miért ne legyen biztonságosabb a kódunk, és ha valami nem várt körülményt észlelünk, akkor azonnal leállunk?
Szándékaink kifejezése
Az assert-ek tesztkódon kívüli használata nem csupán belső állapotellenőrzésre alkalmazható, hanem többi fejlesztőtársunk felé is kommunikálhatjuk vele feltételezéseinket. Nekem nagyon sokat segített már az, hogy a régebben saját vagy más által megírt kódban az assert-ek képesek voltak az akkori fejlesztői elme gondolatát még hatékonyabban közvetíteni.
Az assert-ek tudatos használa során nem csupán egy implementáció fogad bennünket egy metódus elolvasásakor, de a megfogalmazott állítások egyfajta kontextusba is helyezik a metódust.
Mikor ne használjunk assert-eket?
Az assert-ek tesztkódon kívüli használata csak belső állapotellenőrzésre szolgálhat. Nem szabad alkalmazni akkor, amikor a “külvilág” bemeneteire reagálunk pl:
- input (pl. UI-ról vagy Web Service-en keresztül érkező) adatok validálásakor: ilyenkor – az alkalmazott technológiától függően – specializált exception-ökkel vagy visszatérési értékkel kell tudatnunk a klienssel a hibát.
- API írásakor: ez ugyanaz mint az előző eset. Gondoljunk példaként a Java SDK-ra, ahol egy üres ArrayList-ből akarjuk törölni a 10. elemet. Az API exception-nel válaszol.
Végezetül egy cikk, ami hasonló gondolatokat fogalmaz meg: http://dobbscodetalk.com/index.php?option=com_myblog&show=Assertions-in-Production-Code-.html&Itemid=29
Kristof Jozsa
Jul 10th, 2009nagyon jó cikk, grats