Hamcrest
Technológiák: JUnit 4.11, Hamcrest 1.3
Bevezetés
Ahogy egyre többet írok unit teszteket, így foglalkoztat a gondolat, hogyan lehet minél hatékonyabb asserteket írni. Elvárásaim ezekkel kapcsolatban a következők. Rövidek legyenek, azaz gyorsan, gond nélkül lehessen ezeket megírni. Más számára is könnyen olvashatóak legyenek. Amennyiben elbuknak, értelmes hibaüzeneteket adjanak. És össze lehessen fűzni akár őket.
Köszönet Lontay Gábornak, aki sokat segített a Hamcrest megértésében.
A poszthoz készült példaprogram elérhető a GitHub-on.
A Wikipedia szerint az asserteknek már a harmadik generációjánál járunk. Kezdetben az assert csak egy logikai kifejezést várt, ami ha a kiértékelés során hamisat adott vissza, a teszt elbukott. (Üzenetet ennek is lehetett paraméterül adni.) A második generációs assertek azonban külön várták az aktuális és az elvárt eredményt, így az üzenetben ki tudták írni, hogy pontosan mik nem stimmeltek.
A harmadik generációs assertek azonban támogatják az assertThat
kifejezést, mely várja az aktuális értéket, valamint egy ún. Matcher
objektumot.
assertThat([ellenőrizendő érték], [matcher]);
Egy példával talán érthetőbb:
Ebből az látszik, hogy fluent interfésszel rendelkezik, és primitív matcherekből különböző konstrukciókkal, pl. logikai műveletekkel, bonyolultabbakat lehet összerakni. Gyakorlatilag ezzel egy DSL-t, azaz egy primitív saját nyelvet rakhatunk össze objektumok (egyezőségének) vizsgálatára, ami inkább deklaratív, mint procedurális. Sőt, a hibaüzenetek is olvashatóak maradnak.
Ehhez képzeljük el, hogy nem csak szöveges összehasonlítások vannak, hanem pl. collectionök kezelésére való műveletek is, pl. olyan feltételeket tudunk szabni, hogy egy vagy több elem benne van-e a collectionben, megfelelő sorrendben-e, stb. Ezen kívül még saját matchereket is lehet implementálni.
Hamcrest
Javaban erre a Hamcrest könyvtár használható (, ami a matchers anagrammája). Ezt az eszközt Joe Walnes fejlesztette ki a jMock mock keretrendszer részeként, majd kiszervezte és a Hamcrest nevet adta neki.
A Hamcrest két
rétegből
áll, egyrészt a core
, mely stabil, osztályai nem változnak, valamint a
library
, mely collectionök, stringek, JavaBeanek, stb. kezelésére való
matchereket tartalmaz, és megjelenhetnek újabb és újabb matcherek. Ennek
megfelelően érdemes mindkettőt használni, és a
org.hamcrest.CoreMatchers
osztály helyett a org.hamcrest.Matchers
osztály metódusait statikusan importálni.
Ha már az olvashatóság az egyik legnagyobb előny, nézzük, hogy mi
történik klasszikus esetben, és mi az assertThat
-tel. A JUnit még
tartja magát.
És nézzünk egy bonyolultabb feltételt:
Sőt, az assertThat
egy override-olt metódusa String
-et is elfogad.
Ahogy a tutorial is írja, rengeteg core matcher jön a Hamcresttel, erről egy jó kis egy oldalas referencia PDF is található.
Nyilván vannak általános célú matcherek, mint egyenlőség ellenőrzés,
relációk ellenőrzése, null ellenőrzés, String
-ek összehasonlítása,
stb. Érdemes ezt a cikket
is elolvasni. Ami talán érdekesebb az a JavaBeanek és a kollekciók
kezelése.
HasProperty
A hasProperty
metódussal azt nézhetjük meg, hogy egy adott objektumnak
a megadott property-je nekünk megfelelő-e.
Collections
A collectionök összehasonlítására a leggyakrabban használt metódusok a
contains
, containsInAnyOrder
, hasItem
, hasItems
és isIn
.
A contains
elnevezése nagyon megtévesztő, itt ugyanis a két collection
pontos egyezőségét várja el, a sorrendet is beleértve.
Más sorrendben adva meg az értékeket, a teszt elbukik, akárcsak akkor, ha egyet kihagyunk. Ekkor szép hibaüzenetet is kapunk:
java.lang.AssertionError:
Expected: iterable containing ["CIB VISA Inspire", "CIB MasterCard Gold", "CIB Vis Internetkártya"]
but: item 2: was "CIB Visa Internetkártya"
... stacktrace
A hasItem
, hasItems
már az elnevezésének megfelelően működik, itt
ugyanis tényleg azt nézi, hogy ténylegesen szerepel-e az elem(ek) a
collectionben.
Az isIn
metódussal fordítva adhatjuk meg:
Ha a hasItem
metódusnak matchert akarunk paraméterül átadni, hamar
meglepetés érhet. A következő kód ugyanis nem
működik:
Helyette a következő trükköt kell alkalmaznunk:
Érdekessége, hogy míg a csak List
-té castolást az IDEA/Eclipse
elfogadja, parancssori fordításkor elszáll, így kell a List<Object>
-té
castolás is.
Saját Matcher implementálása
A matcherek a Matcher
interfészt implementálják, de mi ne ezt
implementáljuk, hanem induljunk ki valami speciális implementáló
osztályból, melyből leszármaztathatunk. Amennyiben egy objektum egy
értékét akarjuk hasonlítani, akkor
használjuk
a FeatureMatcher
osztályt. ennek featureValueOf
metódusa adja vissza
a kívánt értéket.
És ezt a következőképpen használhatjuk fel:
Vagy hasonlóan egy másik FeatureMatcher
implementációt, de már
collectionre:
Ha még ennél is messzebb akarunk menni, akkor egy
TypeSafeDiagnosingMatcher
leszármazottat érdemes készítenünk (, ez
amúgy mely a FeatureMatcher
őse).
És a használata:
Ha ez sem elég, akkor használjuk a BaseMatcher
osztályt, ennek
leszármazottja a TypeSafeDiagnosingMatcher
.
A példákban látható, hogy factory metódusokat alkalmaztunk, mely
lehetővé teszi a könnyebb olvashatóságot az assertThat
metódus
paraméterként. Valamint elláttuk a @Factory
metódussal. Ez arra való,
hogy a Hamcrestben van egy generátor, mely képes kigyűjteni ezen
metódusokat egy osztályba, így nem kell minden osztályt importálnunk. A
org.hamcrest.Matchers
oszály is így készül.
Jó tanácsok
Amennyiben matchereket használ vagy ír az ember, először mindenképp próbáljuk ki a negatív ágat, hogy használható hibaüzenetet ír-e ki.
Megoszlanak arról a vélemények, hogy egy teszt esetben egy assertet lehet-e csak használni. Én nem vagyok ennyire szigorú, szimpatikus a következő megfogalmazás: “test one logical CONCEPT per test”. Ez általában akkor teljesül, ha ugyanazon az objektumon végzel assertet, logikailag összetartozó feltételeket adsz meg. Gyakran egyszerűbb kifejezni és olvasni, ha több assert kifejezést használsz.
Történeti háttér
A JUnit fejlesztői úgy
döntöttek,
hogy a JUnit részévé teszik. A 4.4
verzióban jelent
meg, gyakorlatilag a hamcrest-core
projekt osztályait a JUnitba
másolták. Ez volt az első alkalom, hogy egy third-party library bekerült
a JUnitba, ami kifejezetten híres volt arról, hogy nem akart plusz
függőségeket. Persze a régi assert metódusok, pl. assertEquals
,
assertTrue
, stb. megmaradtak. Megjelent tehát a org.hamcrest
csomag,
benne a CoreMatchers
osztály, valamint az org.junit.matchers
csomagban a JUnitMatchers
osztály. Mindez 2007-ben történt.
Sajnos azonban hoztak egy Mavennel szemben álló döntést, ezeket a
class-okat elhelyezték a junit-4.4.jar
artifact-ba. A JUnit csak a
core
osztályait tartalmazta. Ha viszont a library osztályaira is
szükség volt, és az ember csak berakta a JUnit mellé, és nem volt
megfelelő a jar fájlok sorrendje a classpath-on, elég nagy galibát
tudott
okozni.
Ennek megoldására jelent meg a
junit-dep
artifact, mely nem tartalmazza a Hamcrestes osztályokat, viszont van
dependency a hamcrest-core-ra
. Ezt csak a JUnit
4.11-ben
javították, ahol a junit
artifact végre nem tartalmazza a Hamcrestes
osztályokat, hanem egy tranzitív függőség van a hamcrest-core
1.3
(jelenleg legfrissebb) verziójára. Így mindenképpen ezt javaslom
használni. A JUnitMatchers
osztály metódusai deprecated-ek lettek,
helyette a org.hamcrest.CoreMatchers
metódusai használandók.
Azért is érdemes a legutolsó JUnit-ot használni, mert korábbi verzióban
az org.junit.Assert.assertThat
nem használta az ún. mismatch
descriptiont, így a hibaüzenetek nem lettek olyan beszédesek, és
figyelni kellett, hogy MatcherAssert.assertThat
legyen használva.
Szerencsére a JUnt 4.11 verziójában ezt már javították.
Úgy látszik, hogy ebben a körben nagyon népszerű a különböző library-k
összecsomagolása. A Mockito esetén is van egy mockito-all
artifact,
mely a Hamcrest és az Objenesis
osztályait csomagolja egybe. Ez akkor okoz kellemetlen meglepetést,
mikor a Hamcrest egy újabb verzióját akarjuk használni, és nem tűnik
fel, hogy a régebbi verzió osztályai már ott vannak a classpath-on.
Egyebek
Persze lehet rengeteg matchert találni a neten is, pl. szövegre, dátumra, Excel, JSON és XML formátumokra is, stb.
Megjegyzendő, hogy a Hamcrest nem csak JUnittal együtt hasznos, hanem más eszközök is használják. Több nyelvre is portolták, mint Python, Ruby, Objective C, PHP, Erlang, ActionScript.