A Javascript programozási nyelv

JS Unit Test

Bevezetés

Az alábbiakban néhány JavaScript nyelvre alkalmazható Unit Test rövid leírása található.
A kliens oldali egységek (Unit) tesztelésének egy igen nagy nehézsége az egységek hiánya. Ez többek között azért is fordulhat elő, mivel a JS kódok egy-egy weboldalon vagy egy alkalmazás különböző moduljaira külön-külön íródott háttér-, felületi logika és HTML oldalak egyvelegét alkotva. Továbbá a HMTL oldalakon gyakran használt események is megnehezítik a dolgunkat. Illetve a DOM használata is bonyolítja a tesztelést, mivel az egyes függvények a különböző DOM értékek alapján különböző eredményeket adhatnak.

QUnit

Az alábbiakban a QUnit eszközön keresztül adunk egy kis bevezetést a JS Unit Test-elésbe. Először vegyünk egy egyszerű példafüggvényt:

function cf(a,b){ return a*Math.sqrt(b); }

Teszteléshez elég letöltenünk a QUnit oldaláról magát az eszközt tartalmazó .js fájlt, illetve a szebb megjelenítés érdekében a .css fájlt. Ezeket belinkelve a kódba már alkalmazhatjuk is:

<!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <title>Example of extracting root of negative real</title> <link rel="stylesheet" href="qunit-1.11.0.css" /> <script src="qunit-1.11.0.js"></script> <script src="cf.js"></script> <script> test("cf test", function(){ equal(cf(1,1),1); equal(cf(2,4),4); equal(cf(0,0),0); equal(cf(0,-1),0); }); </script> </head> <body> <div id="qunit"></div> </body> </head>

Ebben a példában egy egyszerű </div> tag-be rakjuk teszt eredményét. A tesztek futtatására a test használható, aminek paraméterében megadhatjuk a teszt nevét, illetve a lefuttatandó teszteket. Jelen esetben egy függvény törzsében hívjuk meg több esetben a cf függvényt, egy a*sqrt(b) műveletet hajt végre, (a,b) paraméterek esetén. A függvény eredményét az equal metódussal tudjuk ellenőrizni. Előredefiniált paraméterátadás esetén a várt értékkel egyeztetjük. Ha a eredménye nem egyezik meg a várt eredménnyel, akkor a teszt jelenezni fog.
Ha megnyitjuk a html oldalt egy böngészőben (ügyelve hogy a fájlstruktúrában a html oldallal egy mappában helyezkednek el a QUnit .js, ill. .css fájljai, vagy esetleg átírva az elérési utat), látható egy kellemes környezetben a tesztek eredménye. Mégpedig a 4 tesztből 3 sikeres, 1 sikertelen, ugyanis az utolsó esetben a gyök alatt negatív érték szerepel, ami JavaScript-ben NaN értéket eredményez. Ugyanakkor a szorzás eredménye a komplex számsíkon 0 lenne, így a várt értéknek 0-t adunk meg. Ez eset könnyen lekezelhető egy feltétel ellenőrzésével:

function cf(a,b){ if (a == 0) { return 0; } else { return a*Math.sqrt(b); } }

És így már minden tesztünk sikeresen lefut. Nyilvánvalóan ez a példa nem túl gyakorlatias és nagyon leegyszerűsített, de jól tükrözi, hogy egyes bizonyos esetek lekezelésének hiányát könnyedén ki lehet szűrni egy-egy ilyen teszttel.

DOM tesztelése

Ezek után vegyünk egy DOM objektumokkal foglalkozó kódot. Ezúttal a függvényünk legyen az alábbi:

function getLastName(str) { arr = str.split(' '); return arr[arr.length-1]; } function updateName(divid) { var pars = document.getElementById(divid).getElementsByTagName("p"); for ( var i = 0; i < pars.length; i++ ) { var name = getLastName(pars[i].innerHTML); if (name) { pars[i].innerHTML = name; } } }

Ez nem csinál mást, mint a paraméterben átadott string szavait, elválasztó jelként a szóközt tekintve, egy tömbbe szúrja be, majd visszaadja annak utolsó elemét, azaz az utolsó szót. A mi esetünkben bemenetként egy angol írásmódú nevet várunk, és visszaadjuk a családnevet. Továbbá tekintsük az alábbi metódust, ami végigiterál a paraméterben átadott azonosítóval rendelkező </div> tag paragrafusain (</p> elemek), majd a tartalmukat frissíti az eredeti tartalmon lefuttatott getLastName() függvény eredményével. Legyen a tesztoldalunk az alábbi:

<!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <title>Example of cutting last name</title> <link rel="stylesheet" href="qunit-1.11.0.css" /> <script src="qunit-1.11.0.js"></script> <script src="namefunc.js"></script> <script> test("Last name test", function(){ equal(getLastName("David Gilmour"),"Gilmour"); equal(getLastName("Roger Waters"),"Waters"); equal(getLastName("J. R. R. Tolkien"),"Tolkien"); }); test("UpdateName", function() { var pars = document.getElementById("qunit-data").getElementsByTagName("p"); equal(getLastName(pars[0].innerHTML),"Morrisons"); equal(getLastName(pars[1].innerHTML),"Maestro"); updateName("qunit-data"); equal(pars[0].innerHTML,"Morrisons"); equal(pars[1].innerHTML,"Maestro"); }); </script> </head> <body> <div id="qunit"></div> <div id="qunit-data"> <p>Jim Morrisons</p> <p>Shai Maestro</p> </div> </body> </head>

Az első teszt ellenőrzi néhány névre a getLastName() működését. A második teszt ellenőrzi az paragrafusok tartalmát, majd az updateName() meghívása után ismét ellenőrzi azokat, amiknek már csak az utolsó nevét kell tartalmazniuk. Láthatóan mindkét teszt sikeres.

Assertion

Korábban már használtuk az equal függvényt. A QUnit összesen 3 féle visszajelzést biztosít a teszteléshez:

Használatukra egy példa:

<!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <title>Example of assertions</title> <link rel="stylesheet" href="qunit-1.11.0.css" /> <script src="qunit-1.11.0.js"></script> <script> test( "ok test", function() { ok( true, "true succeeds" ); ok( "non-empty", "non-empty string succeeds" ); ok( false, "false fails" ); ok( 0, "0 fails" ); ok( NaN, "NaN fails" ); ok( "", "empty string fails" ); ok( null, "null fails" ); ok( undefined, "undefined fails" ); }); test( "equal test", function() { equal( 0, 0, "Zero; equal succeeds" ); equal( "", 0, "Empty, Zero; equal succeeds" ); equal( "", "", "Empty, Empty; equal succeeds" ); equal( 0, 0, "Zero, Zero; equal succeeds" ); equal( "three", 3, "Three, 3; equal fails" ); equal( null, false, "null, false; equal fails" ); }); test( "deepEqual test", function() { var obj = { foo: "bar" }; var t; deepEqual( obj, { foo: "bar" }, "Two objects can be the same in value" ); deepEqual( t, undefined ); deepEqual( t, null ); }); </script> </head> <body> <div id="qunit"></div> </body> </head>

Szinkron Callback

A QUnit biztosít speciális visszajelzést, amivel meg tudjuk határozni, hogy mennyi visszajelzést várunk a teszt végrehajtása alatt. Ha nem a várt számú visszajelzés történik, a teszt elbukik, függetlenül a többi visszajelzés eredményétől. Ezek számát az expect() függvénnyel tudjuk meghatározni. Így a Callback függvények ellenőrzésére is kapunk egy kézenfekvő megoldást:

<!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <title>Example of synchronous Callback</title> <link rel="stylesheet" href="qunit-1.11.0.css" /> <script src="qunit-1.11.0.js"></script> <script> test( "a test", function() { expect( 2 ); function calc( x, y, operation ) { return operation( x , y ); } var result = calc( 2, 7, function( x, y ) { ok( true, "calc() calls operation for "+2+" and "+7); return x + y; }); equal( result, 9, "2+7 equals 9" ); }); </script> </head> <body> <div id="qunit"></div> </body> </head>

Itt az expect() függvénnyel beállítjuk 2-ra a várt visszajelzések számát, definiáljuk a calc() Callback függvényt, ami elvégzi az átadott operandusokra az átadott operátort, majd visszaadja ennek eredményét. A result változóban eltároljuk a calc() függvény eredményét, olyan operandussal meghívva, ami tartalmaz egy visszajelzést. Ezzel biztosítjuk, hogy csak a calc lefutása esetén legyen sikeres a teszt. Végül ellenőrizzük a számítás eredményét.

Aszinkron Callback

Aszinkron Callback függvények tesztelésére az asyncTest() függvényt lehet használni:

<!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <title>Example of Asynchronous Callback</title> <link rel="stylesheet" href="qunit-1.11.0.css" /> <script src="qunit-1.11.0.js"></script> <script> asyncTest( "asynchronous test: one second later!", function() { expect( 1 ); setTimeout(function() { ok( true, "Passed and ready to resume!" ); start(); }, 1000); }); </script> </head> <body> <div id="qunit"></div> </body> </head>

A start függvény meghívásával jelezhetjük, hogy a teszt blokkja befejeződött, kész a visszatéréshez. Ebben az esetben 1 visszajelzést várunk, így az expect metódus az 1 paraméterrel hívjuk meg. A setTimeout arra szolgál, hogy egy meghatározott idő múlva meghívja az átadott függvényt aszinkron, s közben a vezérlés tovább haladjon. Azonban a testAsync csak akkor kezdi el ellnőrizni a visszajelzésetket miután meghívtuk a start metódust. Így a helyett, hogy a teszt a blokk végére érve hibásnak ítélné végrehajtást, a visszajelzést hiányában, megvárja a start meghívását, és csak utána ellenőrzi.

Felhasználói akciók

A felhasználói akciókon alapuló kód általában nem tesztelhető megfelelően csupán függvények meghívásával és azok eredményeinek vizsgálatával. Az ilyen események teszteléséhez használhat a jQuery trigger() függvénye. Ha nem akarjuk a kiváltani az eseményt, akkor a triggerHandler() függvény meghívásával tudjuk szimulálni a hozzá kötött akciót. Például ha egy linken akarjuk vizsgálni a click() eseményt igen hasznos lehet, ugyanis ilyenkor általában az esemény kiváltódása után megváltozik a cím. Vegyük a QUnit példáját:

function KeyLogger( target ) { if ( !(this instanceof KeyLogger) ) { return new KeyLogger( target ); } this.target = target; this.log = []; var self = this; this.target.off( "keydown" ).on( "keydown", function( event ) { self.log.push( event.keyCode ); }); }

Itt definiálunk egy KeyLogger() függvényt, ami a paraméterben átadott objektumot beállítja target mezőjébe, leköti a keydown() eseményről Callback függvényét, majd hozzáköt egy függvényt, ami hozzáadja a KeyLogger() log mezőjéhez a keydown() esemény billentyűkódját. Legyen a teszt szintén a QUnit példája kódja:

test( "keylogger api behavior", function() { var event, $doc = $( document ), keys = KeyLogger( $doc ); // trigger event event = $.Event( "keydown" ); event.keyCode = 9; $doc.trigger( event ); // verify expected behavior equal( keys.log.length, 1, "a key was logged" ); equal( keys.log[ 0 ], 9, "correct key was logged" ); });

Ez a KeyLogger()-errel létrehozott keys objektum target attribútumába beállítja a document objektumot. Szimuláció képpen kontruál egy keydown eseményt, aminek a billentyűkódját 9-re állítja, majd ellen document objektumra kiváltja azt és ellenőrzi a keys log-ját, hogy tartalmazza-e a megfelelő billentyűkódot. Ne felejtsük el letölteni a jQuery kódját és megfelelően beimportálni!
Ha a tesztelendő eseménykezelő nem az esemény valamely speciális paraméterén alapul, akkor természetesen elég csak az eseményt kiváltani. Ha azonban használ valami ilyen paramétert, akkor a $.Event eseménnyel létre is kell az objektumát.
Fontos, hogy minden a teszt szempontjából releváns eseményt kiváltsunk. Például egy dragging() esemény egy egérlenyomás, legalább egy egérmozgatás és egy egérfelengedés eseményből épül fel. Hasonlóan, az egyszerűnek tűnő click() esemény valójában egy egérlenyomás és egy egérfelengedés is egyben.

Egyéb Unit Test környezetek

Ugyan a QUnit-al igen sok esetet lehet ellenőrizni, például a node.js féle kliens/szerver koncepció tesztelését. Az alábbiakban néhány egyéb, jóval több lehetőséget kínáló eszközt sorolunk fel a közvélemény megjegyzéseivel.

TestSwarm

John Resig, a jQuery megalkotója által fejlesztett eszköz a JavaScript elosztott teszteléséhez. Leginkább nyílt forráskódú JS projektek teszteléséhez készült.

Előnyei:
Hátrányai:

JsTestDriver

A Google által fejlesztett () eloszotott JS eszköz. Hasonlaón a TestSwarm-hoz van kliens/szerver kapcsolat, de támogatja a parancssorból való tesztelést is.

Előnyei:
Hátrányai:

Buster.js

A JsTestDriver-hez hasonló szerver/kliens koncepciójú eszköz, kivéve hogy a szervert JS-ben (node.js) írták Java helyett. Ennek megfelelően automatikusan lefuttatja a teszteket. QUnit-hoz hasonló statikus HTML oldalakban írhatóak a tesztek, ún, fej néküli böngészőkben tesztel (phantomjs, jsdom, …).

Előnyei:
Hátrányai:
-