Testování Javascriptových projektů IV

od aichi E-mail

V minulém díle jsme se seznámili s frameworkem Jasmine a ukázali si, jak v něm psát unit testy. Dnes si ukážeme testování asynchronních funkcí a také použití "špionů" pro sledování interních stavů objektů.

...

Testování asynchronního kódu

V Javascriptu je mnoho funkcionality tvořeno asynchronním kódem, který je vyvolán po uživatelově akci, po stažení souboru ze serveru, nebo po určité době.

Představme si, že vytváříme program, který bude prezentovat náročné matematické výpočty a jednou z funkcí bude výpočet Fibonacciho posloupnosti: 0 1 1 2 3 5 8 13 21 34 55 89 144 ... Výpočet hodnoty člena zadaného pořadím je dán funkcí:

function f(n) {
    if(n == 0) return 0;
    if(n == 1) return 1;
    return f(n - 1) + f(n - 2);
}

Nyní jsme dostali za úkol zobrazit uživateli řadu hodnot posloupnosti v délce zadané uživatelem. Pro tento případ můžeme vytvořit tuto metodu:

function show(n) {
    var i,
        value ="";
   
    for(i = 0; i <= n; i++) {
        value += (i == 0 ? "" : " ") + f(i);
    }
    return value;
}

Jako při reálném vývoji budeme chtít napsat test, který otestuje metodu show.

describe("Fibonacci tests", function(){
			it('should return the Fibonacci sequence for first 40 numbers', function(){

				expect(show(40)).toEqual('0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181 6765 10946 17711 28657 46368 75025 121393 196418 317811 514229 832040 1346269 2178309 3524578 5702887 9227465 14930352 24157817 39088169 63245986 102334155');
			});
});

Po spuštění v rychlých prohlížečích se zhrozíme, neboť vykonání testu trvá minimálně 12s - neúměrně dlouho. Je na čase vyřešit výpočet lépe. První co nás napadne je, modifikovat funkci show tak, aby bylo možnost zadat callback funkci, která bude zavolána po dokončení výpočtu:

function show(n, callback) {
    var i,
        value ="";
   
    for(i = 0; i <= n; i++) {
        value += (i == 0 ? "" : " ") + f(i);
    }
    callback(value);
}

Bohužel tímto si moc nepomůžeme, protože je pěkné, že funkce může pracovat a výsledek obdržíme v callbacku, ale jak víme, Javascript je jednovláknový a proto je tento postup vlastně stejný jako první řešení. pro n > 30 se nám ve Firefoxu 3.6 objeví okno ohledně blokujícího Javascriptu. Tomu se chceme vyhnout, proto funkci vytvoříme kompletně asynchronní:

(function(){
var callbackFunc,
    i,
    N,
    value='',
    interval,
    comp = function() {
        for(var k = i+5;i < k && < <= N; i++) {
            value += (i == 0 ? "" : " ") + f(i);
        }
        if (i == N) {
            clearInterval(interval);
            interval = null;
            callbackFunc(value);
        }
    };

window.show = function(n, callback) {
    if (interval) return false;

    N = n;
    callbackFunc = callback;
    i = 0;

    interval = setInterval(comp,20);
}
})()

Nyní již můžeme počítat větší hodnoty, nicméně narazíme na limit funkce f, kdy pro výpočet hodnoty prvku s vysokým pořadím musíme počítat stále dokola prvky předchozí. Výsledek snažení si můžeme vypsat pomocí tohoto příkazu v konzoli Firebugu:

show(30, function(v){console.log(v)}) 

Psaní asynchronního testu

Problém nastane při spuštění našeho testu. Test skončí chybou: Expected undefined to equal '0 1 1 2 3 5 8 13 21 34 55 89 144 ... 102334155'. Tato chybová zpráva nám napovídá, že jsme neobdrželi výsledek při zavolání funce show. Musíme tedy test upravit tak aby počkal na výsledek a otestoval až hodnotu předanou callback funkci. Leckdo by očekával, že stačí test upravit takto:

describe("Fibonacci tests", function(){
    it('should return the Fibonacci sequence for first 40 numbers', function(){
        show(5, function(res){
            expect(res).toEqual('0 1 1 2 3 5 8 13 21 34');
        });
    });
});

Tedy spuštění testu až v callback funkci. Ale při spuštění testu bude výsledek zelený i když náš test je záměrně chybný (očekáváme 10 čísel, ale funkci show voláme s parametrem 5). Proč? Protože vyhodnocení testu nečeká na callback funkci a výsledek metody expect není k tomuto testu přiřazen. Musíme tedy vyhodnoceni pozdržet.

K tomuto pozdržení nám slouží metody runs, waits a waitsFor. Tyto metody serializují náš kód. Obalíme-li náš kód do metody runs je vykonáno ihned. Nicméně, pokud máme víc runs bloků za sebou, jsou vykonávány sériově.

Zajímavější je metoda waits, jejímž parametrem je počet milisekund, které má čekat a poté vykoná předaný kód. Poslední metoda waitsFor má jeden povinný parametr a to funkci. Jasmine pozdrží vykonání následujících bloků, dokut tato funkce nevrátí true.

describe("Fibonacci tests", function(){
    it('should return the Fibonacci sequence for first 40 numbers', function(){
        var res = '',
            callback = function(result){
                res = result;                        
            };
        
        runs(function(){
            show(5, callback);          
        });
        
        waitsFor(function(){
            return res !== '';
        });
        
        runs(function(){
            expect(res).toEqual('0 1 1 2 3 5 8 13 21 34');
        });
    });
});        

Nyní je výsledek testu červený, tudíž testujeme reálný výsledek. Jak test funguje? Na začátku si připravíme callback funkci, která do globální proměnné res zapíše výsledek. Prvním blokem runs spustíme výpočet a blokem waitsFor pozdržíme vykonání testu dokud výsledek nebude v globální proměnné res.

Před nasazením špionů provedeme jednu aktualizaci. Našim šéfům se nelíbila možnost volání metody show asynchronně, protože chtějí výsledky hned, musíme zrychlit funkci f použitím cache:

(function(){
	var cache = [];
	window.f = function(n) {
		if (n == 0) return 0;
		if (n == 1) return 1;
		if (cache[n]) return cache[n];
		else return cache[n] = f(n - 1) + f(n - 2);		
	}
})();

Jelikož nyní počítáme každou hodnotu pouze jednou, je metoda dostatečně rychlá na to, abychom ji volali synchronně a tudíž se můžeme vrátit k původní metodě show:

function show(n) {
    var i,
        value ="";
   
    for(i = 0; i <= n; i++) {
        value += (i == 0 ? "" : " ") + f(i);
    }
    return value;
}

Samozřejmě se musíme rozloučit se svým úžasným asynchronním testem, ale to je běžný chléb agilního programátora.

Nasazení špionů

Představme si, že ve vaší aplikaci využíváte XMLHttpRequest k získání dat ze serveru. Při psaní Unit testů není žádoucí se spoléhat na externí nespolehlivé zdroje, neboť pak netestujeme vlastní jednotku ale i externí systém. Proto se ujali tzv. Mock objekty, tedy objekty, které se tváří jako originál, ale nedělají to co originál. Takže můžeme např. využívat vaší oblíbenou knihovnu a jen upravit její funci tak aby vracela vždy předpokládaný výsledek, nebo prostě upravit metody XMLHttpRequestu.

Tyto úpravy podporuje i knihovna Jasmine pomocí funkce pro vytvoření špiona spyOn. V našem konkrétním případě bychom chtěli zjistit zda je funkce f volána z show a jaký argument je jí předán.

describe("Fibonacci tests", function(){
    it('should call function "f()"', function(){
    	spyOn(window, 'f');
    
        show(5);
		
	expect(f).toHaveBeenCalled();
	expect(f).toHaveBeenCalledWith(5);            
    });
});

Na prvním řádku testu vytvoříme špiona funkce f. Tento špion nahrad vlastní funkci a uchovává informace o počtu volání a posledních předaných argumentech. Tyto dvě vlastnosti také v testu testujeme.

První Matcher, toHaveBeenCalled, vrací boolean hodnotu podle toho, zda funkce byla volána. Druhý, toHaveBeenCalledWith, testuje, jaké argumenty byly volané funkci předány. Můžeme také testovat kolikrát byla daná funkce volána pomocí testu hodnoty callCount:

toBe(6);

V našem aktuálním příkladu špion nahradil náši funkci vlastní. To se hodí při nahrazování funkčnosti zmíněného XMLHTTPRequestu, nebo jiné externí knihovny. Pak samozřejmě můžeme navolit i návratovou hodnotu pomocí funkce andReturn špiona:

describe("Fibonacci tests", function(){
    it('should call function "f()"', function(){
    	spyOn(window, 'f').andReturn('0');
    
        expect(show(5)).toEqual('0 0 0 0 0 0');   
		
	expect(f).toHaveBeenCalled();
 	expect(f).toHaveBeenCalledWith(5);         
    });
})

Ano, test je přitažen za vlasy, protože ordinujeme návratovou hodnotu a tudíž testujeme bludy. Berte tudíž tento test jen jako ukázku možností.

Asi nejzajímavější možností je pomocí špiona sledovat obalenou funkci, ale tuto funkci také použít a vrátit její návratovou hodnotu. Potom náš příklad výše bude opět správným testem:

describe("Fibonacci tests", function(){
    it('should call function "f()"', function(){
    	spyOn(window, 'f').andCallThrough();
    
        expect(show(5)).toEqual('0 1 1 2 3 5');   
		
	expect(f).toHaveBeenCalled();
 	expect(f).toHaveBeenCalledWith(5);         
    });
})

Co můžeme ještě "vyšpionit"? Například by nás mohla zajímat statistika argumentů použitých při volání obalené funkce. Špion si do interního objektu mostRecentCall ukládá nejčastěji použitou sadu argumentů a do pole argsForCall jednotlivé sady argumentů volání tak, jak šli chronologicky za sebou.

Objekt mostRecentCall můžeme využít pro test, kdy chceme zjistit, zda 0 byla nejčastějším argumentem. (Toto může být pravna pro originální funkci, bohužel ne pro naši funkci s cache, jelikož ta je volána pro každé číslo právě jednou.)

expect(f.mostRecentCall.args).toBe(0);  

Špion dále obsahuje metody andThrow(msg) pro vyvolání vyjímky při zavolání a andCallFake(func) pro zavolání předané anonymní funkce místo funkce obalené.

Závěr

Ukázali jsme si pokročilé možnosti testování frameworku Jasmine. Posledním větším tématem je ukázat si jak napsat vlastní Matcher a také jak použít výstup pro automatizované testování. Ale o tom až někdy příště.

Adresy zpětných odkazů pro tento příspěvek:

Trackback URL (right click and copy shortcut/link location)

Zatím žádná reakce

Napsat komentář


Vaše e-mailová adresa nebude zveřejněna.

Adresa Vašich WWW stránek bude zveřejněna.
(Konce řádku budou převedeny na <br />)
(Jméno, email a webová stránka)
(Dovolí ostatním uživatelům kontaktovat Vás prostřednictvím formuláře pro zprávy (Vaše e-mailová adresa NEBUDE zveřejněna.))