Az ERLANG programozási nyelv

Tesztelés

Bevezetés

Alapvetően kétféle tesztelés létezik, az egyik a manuális, a másik pedig az automata tesztek. Manuális amikor kézzel végigellenőrizzük a program egyes állapotait. Ezt persze egyszer meg kell tennünk, de utána már időigényes ezekkel foglalkoznunk. Ezért vannak az automatizált tesztek, amiről ez a fejezet szólni fog. Ezek a tesztek arról szólnak, hogy a programozó megír egy külön programot, ami leteszteli a különböző eseteket, hogy azokat később már kézzel ne kelljen, így csak akkor kell foglalkoznia vele újra a programozónak, ha valamiért hibával áll le a teszt. Az Erlang programok automatateszteléshez sok eszköz létezik, itt az EUnit-ot fogjuk bemutatni.

TDD

A TDD a Test-driven development rövidítése, vagyis tesztek által vezérelt fejlesztést jelent. A lényege, hogy a először teszteket írunk a készítendő program specifikációjához mielőtt még bármilyen kódot írtunk volna a programból. Ezeken a teszteken, ha a megírt program átmegy, akkor azt jelenti, hogy megfelel a program a specifikációknak. Ez segíthet a programozónak, hogy szem előtt tartsa a megoldandó problémákat és ne készítsen komplikált implementációkat, ha nem muszáj

A tesztelés fontossága

A programok többsége változni fog az idő során, hibák lesznek benne javítva, új funkciók lesznek hozzáadva, optimalizálás során megváltozik, vagy csak refaktorálva lesz a kód, hogy könnyebben tudjanak vele dolgozni. Bárhogy is történik, ezek a változtatások, új hiba lehetőségeket vonnak maguk után, vagy akár előidézhetnek már régebben előjött és javított hibákat is. Tesztek által garantálhatjuk azt, hogy egyes programrészek futnak, és úgy ahogy azoknak futniuk kell.

EUnit

A legegyszerűbb módszer, hogy használjuk az EUnit-ot az Erlang modulunkban az, hogy hozzáadjuk a következő sort a modulunk elejére:

-include_lib("eunit/include/eunit.hrl").

Ennek a sornak a hatására a következők fognak történni:

Hogyan is használjuk az EUnitot?

Tegyük fel, hogy nem TDD-ben programozunk, és már megvan írva a következő mylist.erl modulunk:

-module(mylist). -export([sum/1,product/1,odds/1]). sum([]) -> 0; sum([Head | Tail]) -> Head + sum(Tail). product([]) -> 1; product([Head | Tail]) -> Head * product(Tail). odds(List) -> odds(List,1). odds([Head | Tail],N) when N rem 2 == 1 -> [Head | odds(Tail,N+1)]; odds([_|Tail],N) -> odds(Tail,N+1); odds([],_) -> [].

A modulban három függvény található:

Sikeres tesztek

Ezeket a függvényeket egy különálló modul fogja tesztelni, a neve annak mylist_tests.erl lesz (megj. célszerű valamilyen konvenciót használni a teszt modulokra, így könnyebben átlátható lesz a projektünk később is)

-module(mylist_tests). -include_lib("eunit/include/eunit.hrl"). sum_test() -> ?assertEqual(0, mylist:sum([])), ?assertEqual(0, mylist:sum([0])), ?assertEqual(6, mylist:sum([1,2,3,4,-4])). product_test() -> ?assertEqual(1, mylist:product([])), ?assertEqual(2, mylist:product([2])), ?assertEqual(36, mylist:product([2,3,2,3])). odds_test() -> ?assertEqual([], mylist:odds([])), ?assertEqual([1], mylist:odds([1])), ?assertEqual([1,3,5], mylist:odds([1,2,3,4,5])).

Látható, hogy megadjuk a tesztelni kívánt függvényekhez, hogy milyen paraméterezéssel, milyen eredményt várunk. Erre szolgál az assertEqual makró
Hasonló a célja, mint más nyelvekben, mint például java-ban a TestNG. Ezeknek a célja, hogy kiértékeli valamilyen szempontból a paraméterként kapott értékeket, és vagy a false vagy a true atommal tér vissza. Utóbbinál a feltétel teljesül, tehát a program az elvárt szerint működik, és a teszt folytatódhat tovább, ellenkező esetben pedig nem az elvárt szerint működik, tehát a teszt hibát fog jelezni azon a függvényágon.
Továbbá, ahogy írtuk a függvények _test() -re végződnek, hogy majd később ne kelljen egyesével meghívni őket, hanem az EUnit lefuttassa helyettünk.

További beépített assert makrók:
assert, assertNot, assertMatch, assertEqual, assertException, assertError, assertExit, assertThrow, assertCmd, assertCmdStatus, assertCmdOutput

Fordítás és futtatás

Egyszerűen lefordítjuk a mylist.erl és a mylist_tests.erl fájlunkat, és utána kiadjuk a következő parancsot:

eunit:test(mylist).

Tehát arra a modulra hívjuk meg, amelyen futtatni szeretnénk a teszteket.

Kimenet
All 3 tests passed.

A tesztek sikeresen lefutnak, bár szebb lenne a teszteket külön elhelyezni a tesztelendő programoktól.
Hozzunk létre külön mappát a teszt fájloknak, a tesztelendő fájloknak, valamint a beam fájloknak.

$ mkdir src $ mkdir test $ mkdir ebin $ mv mylist.erl src/ $ mv mylist_tests.erl test/

Ezekkel a parancsokkal külön mappákba helyeztük a forrás fájlokat (az src kicsit zavaró név lehet, hisz a teszt fájlok is forrás fájlok)
Ezek után a beam fájlokat pedig szeretnék, ha a fordító az ebin mappába helyezné.

Erre két lehetőség is van. Az első lehetőség, ha erlang shell nélkül fordítunk:

erlc -o ebin/ src/*.erl test/*.erl

A másik lehetőség, hogy az erlang shell indítása során megadjuk, hogy hova rakja a beam fájlokat:

erl -pa ebin/
Részletesebb kimenet

A kimenetre sikeresen kiírtuk, hogy a tesztek milyen eredménnyel zajlódtak, de ez nem túl beszédes. Van azonban egy másik lehetőség is, amikor a tesztekről részletesebb eredményt kaphatunk. Annyit kell módosítanunk, hogy a függvényünket kiegészítjük a verbose, vagyis "bőbeszédű" paraméterrel.

1> eunit:test(mylist,[verbose]). ======================== EUnit ======================== module 'mylist' module 'mylist_tests' mylist_tests: sum_test...ok mylist_tests: product_test...ok mylist_tests: odds_test...ok [done in 0.047 s] [done in 0.047 s] ======================================================= All 3 tests passed. ok

Sikertelen tesztek

Az előbbiekben láttuk, hogy milyen eredményt ad, ha sikeresen lefutottak a tesztjeink. Most nézzük át azokat az eseteket, amikor sikertelen volt, és elemezzük a kimeneti eredményt

Tesztelendő modul
-module(mycalc). -export([fib/1]). fib(0) -> 1; fib(1) -> 1; fib(N) when N > 1 -> fib(N-1) + fib(N-2).
Teszt modul
-module(mycalc_tests). -include_lib("eunit/include/eunit.hrl"). -compile(export_all). fib_function_test() -> ?assert(mycalc:fib(0) =:= 1), ?assert(mycalc:fib(1) =:= 1), ?assert(mycalc:fib(2) =:= 2), ?assert(mycalc:fib(3) =:= 100), ?assert(mycalc:fib(4) =:= 5), ?assert(mycalc:fib(5) =:= 8), ?assert(mycalc:fib(31) =:= 2178309). fib_negative_test() -> ?assertException(error, function_clause, mycalc:fib(-1)).
Kimenet
1> eunit:test(mycalc,[verbose]). ======================== EUnit ======================== module 'mycalc' module 'mycalc_tests' mycalc_tests: fib_function_test...*failed* ::{assertion_failed,[{module,mycalc_tests}, {line,10}, {expression,"mycalc : fib ( 3 ) =:= 100"}, {expected,true}, {value,false}]} mycalc_tests: fib_negative_test...ok [done in 0.031 s] [done in 0.031 s] ======================================================= Failed: 1. Skipped: 0. Passed: 1.
Magyarázat

A fibonacci függvény a 3 paraméterrel eredményül 2-t ad, de mi azt adtuk meg, hogy 100 legyen az eredmény. A kimenetbe látható, hogy a tesztek közül csak az egyik volt sikeres. . Fontos megjegyezni, hogy a többi függvény ág lefut attól függetlenül, hogy az egyik teszt hibás volt. Másik megjegyzés, hogy azonos függvény tesztjei viszont nem futnak le, tehát jelen esetünkben már nem derül az ki, hogy fib(4), fib(5) vagy a fib(31) sikeres lett volna-e.


Látható, hogy a negatív teszt is sikeresen lefutott, pedig ott kivétel történt, hiszen egy nem létező ágat hívtunk meg. A program azonban mégsem ált le, hisz az assertException makrónak épp az a célja, hogy valamilyen kivételt hasonlítson össze. Ilyenkor az EUnit le is kezeli a kivételt és a programozónak nem kell ezzel foglalkoznia vele.

Állapottartó folyamatok tesztelése

Az előző pontokban olyan függvényeket teszteltünk, amelyek adott paraméter értékhez, ugyanazt az eredményt adták. A mostani szakaszban olyan programokat fogunk tesztelni, ahol valamilyen állapotban van éppen a folyamat és a végzett műveletek átviszik egy másik állapotba, és megtartja azt az állapotot. Ezek lehetnek szerverek, egy fsm, de bármi ami megtartja és frissíti az állapotát.

Nézzünk most egy példát állapottartó folyamatra. A példánk egy operátor kiszámoló kis alkalmazás, amellyel egyszerű számolási műveleteket lehet elvégezni

-module(numberserver). -export([start/0,stop/0,op/2,get/0,init/0]). start() -> Pid = spawn_link(?MODULE,init,[]), register(?MODULE,Pid), ok. stop() -> ?MODULE ! stop, unregister(?MODULE). op(Op,Num) -> ?MODULE ! {Op,Num}, ok. get() -> ?MODULE ! {get_result,self()}, receive X -> X end. init() -> loop(basic). loop(E) -> receive stop -> ok; {get_result,From} -> From ! E, loop(E); {Op,Num} -> loop(result(Op,E,Num)) end. result(_,basic,X) -> X; result('+',X,Y) -> X + Y; result('*',X,Y) -> X * Y; result('-',X,Y) -> X - Y; result('/',X,Y) -> X / Y.

Nézzük az exportált függvényeket

Szerver működése

A tesztelés működésének megértéséhez szükséges a szerver modulnak a megértése. A start/0 függvény hatására létrejön egy új folyamat, amely az init/0 függvényt hívja meg ahogy elindul, ezek után beregisztráljuk a modulnévhez a létrehozott folyamat processz azonosítóját. A stop/0 függvénnyel küldünk üzenetet az imént létrehozott folyamatnak, hogy terminálhat, valamint kivesszük a regisztrált nevet, hogy majd újra be lehessen regisztrálni egy új processzt. Az új folyamat a loop/1 függvény receive ágában várja, hogy valaki küldjön üzenetet.

Használjuk először manuálisan a szerverünket

1> numberserver:start(). ok 2> numberserver:op('+',20). ok 3> numberserver:op('*',2). ok 4> numberserver:op('-',10). ok 5> numberserver:op('/',2). ok 6> numberserver:get(). 15.0 7> numberserver:stop(). true 8>

Írjunk egyszerű tesztelést erre!

-module(numberserver_tests). -include_lib("eunit/include/eunit.hrl"). first_additon_test_() -> numberserver:start(), numberserver:op('+',2), numberserver:op('*',3), ?assertEqual(6,numberserver:get()).

Fordítsunk, majd futtassuk a teszteket a szokásos módon, és a következő kimenetet kapjuk

1> eunit:test(numberserver,[verbose]). ======================== EUnit ======================== module 'numberserver' numberserver_tests: first_additon_test (module 'numberserver_tests')...ok [done in 0.015 s] ======================================================= Test passed. ok

Nagyszerű! De mi történik, ha még egyszer futtatjuk?

1> eunit:test(numberserver,[verbose]). ======================== EUnit ======================== module 'numberserver' numberserver_tests: first_additon_test (module 'numberserver_tests')...*failed* ::badarg [done in 0.031 s] ======================================================= Failed: 1. Skipped: 0. Passed: 0. error

Sajnos a szerver még fut, amikor mi újra el akartuk indítani, és nem lehetséges újra beregisztrálni az ugyan olyan nevű numberservert. Tehát valamilyen takarításra van szükség a futtatás előtt.

Ennek a megvalósítására egy 4-es tuple használatára lesz szükségünk

{ setup, SETUP-FUNCTION/0, CLEANUP-FUNCTION/1, TEST/INSTANTIATOR }

Ahol az elemek a következőeket jelentik:

Egy példa állapottartó szerver tesztelésére
-module(numberserver_tests). -include_lib("eunit/include/eunit.hrl"). first_additon_test_() -> { setup, fun setup/0, fun cleanup/1, ?_test( begin numberserver:op('+',2), numberserver:op('*',3), ?assertEqual(6,numberserver:get()) end)}. first_multiply_test_() -> { setup, fun setup/0, fun cleanup/1, ?_test( begin numberserver:op('*',1), numberserver:op('*',3), numberserver:op('*',5), ?assertEqual(15,numberserver:get()) end)}. setup() -> numberserver:start(). cleanup(_Pid) -> numberserver:stop().

Ahogy látjuk, a modul szerkezete abban változott, hogy bejött két segédfüggvény a setup/0 és a cleanup/1. Ezeket az EUnit nem fogja futtatni, hisz nincs a nevük végén a _test(), de használhatóak más függvényből is. A két tesztfüggvénynek is megváltozott a szerkezete, ahogy már említettük, egy négyes tuple-t adunk meg. A 2. és 3. elem ügyel a szerver állapotának konzisztensen tartásához. A 4. elem pedig begin és end között definiálja a tesztlépések sorozatát, amelyet a ?_test makrónak ad paraméterül.

Kimenet
1> eunit:test(numberserver,[verbose]). ======================== EUnit ======================== module 'numberserver' module 'numberserver_tests' numberserver_tests:17: first_multiply_test_...ok numberserver_tests:7: first_additon_test_...ok [done in 0.031 s] [done in 0.031 s] ======================================================= All 2 tests passed. ok

És ezt a tesztet bár hányszor lefuttatva sikeres lesz, mert ügyeltünk, hogy a szerver állapotát megfelelően állítsuk vissza.

Összegzés

A fejlesztés elengedhetetlen része a tesztelés. Egy-egy új szoftver fejlesztése közben szinte ugyanannyi (vagy akár több) időt kell, hogy elvegyen a tesztelés, mint maga a fejlesztés.
Az EUnit-on kívül számos eszköz van az Erlangban tesztelésre, például common Test vagy QuickCheck. Akit bővebben érdekel még az EUnit, az utána tud nézni az Erlang holnapján található EUnit User's guide-on