Witam w ostatnim wpisie z serii “Testowanie z Jasmine”. Dziś na tapetę biorę testowanie operacji asynchronicznych - zresztą w komentarzach pod jednym z wpisów serii, prosił o to jeden z czytelników bloga, a ja obiecałem, że to zrobię więc, tym bardziej czuję się w obowiązku aby ten temat zgłębić i go Wam tutaj jak najlepiej przedstawić :) Tematyka ta wbrew pozorom nie jest szczególnie skomplikowana… No nic, jak zwykle nie ma co przedłużać, przejdźmy od razu do konkretów!

Testowanie operacji asynchronicznych

Testowanie operacji asynchronicznych w Jasmine opiera się na dość prostym mechanizmie pozwalający symulować asynchroniczność testowanych funkcji. Spójrzmy na moduł, zawierający długo trwającą funkcję longTime:

app.async = (function () {

    function longTime(){
        return 10;
    }

    return {
        longTime: longTime
    }

}());

Jak widać, nic niezwykłego - funkcja po prostu zwraca wartość 10. No to spójrzmy teraz na test:

describe('A spec', function() {
    var value;

    beforeEach(function(done){
        setTimeout(function(){
            value = async.longTime();
            done();
        },3000);
    })

    it('should wait until long lasting function call is finished', function(){
        expect(value).toBe(10);
    })
});

Popatrzmy na pierwszą z zaznaczonych linii: zauważcie, że do “callback’a” funkcji beforeEach przekazujemy dodatkową zmienną done. W kolejnej zaznaczonej linii widzimy wywołanie jej jako funkcji - oznacza to, że to również jest funkcja wywołania zwrotnego. To co wymaga wyjaśnienia, to to, że “spec” (funkcja it) nie wykona się dopóki nie nastąpi to wywołanie, a zauważcie, że wszystko to jest “owinięte” w wywołanie funkcji setTimeout, po to aby zaczekać aż przykładowa funkcja longTime zakończy działanie. Dzięki temu, test wykona się dopiero kiedy zmienna value zostanie ustawiona.

To w zasadzie tyle na temat testowania funkcji wykonujących operacje asynchroniczne. Warto jeszcze dodać, że Jasmine **ma **na sztywno ustawiony czas oczekiwania na wykonanie funkcji done - jest on ustawiony na 5 sekund. Na szczęście można go zmienić ustawiając poniższą zmienną na odpowiednią wartość:

jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000;

Mockowanie jQuery AJAX

Oprócz testowania operacji asynchronicznych w sposób opisany powyżej, istnieją też sytuacje gdy nie chcemy testować samej asynchroniczności a jedynie ją “zamockować” i testować tylko zachowanie funkcji asynchronicznej. Popularnym, a do tego szczególnym przypadkiem może być na przykład funkcja ajax biblioteki jQuery, dlatego pomyślałem, że warto w tym miejscu pokazać jak postąpić w jej przypadku.

Przykład

Jak to często u mnie bywa, zaczynamy od razu od przykładu - w końcu dobry przykład jest wart więcej niż tysiąc słów ;) Typowe wywołanie funkcji ajax w jQuery wygląda mniej więcej tak:

$.ajax('remote url address', parameters)
  .done(function(response){
    // success
  })
  .fail(function(response){
    // error handle
  });

Tyle teorii… Spójrzmy więc na przykładowy skrypt, który wykorzystuje powyższe wywołanie:

app.carsService = (function (){
    function search() {
      $.ajax('/cars/get', { id: 9 })
          .done(function(car) {
              app.carsService.car = car;
           });
    }

    return {
        search: search
    }
}());

Jak widzimy, mamy tutaj moduł posiadający metodę search służącą do wyszukiwania samochodu o identyfikatorze równym 9 -oczywiście przydatność biznesową tego przykładu pomijamy :D Metoda ta wykorzystuje funkcję ajax biblioteki jQuery do wywołania zdalnego serwisu, a gdy ten zwróci wynik, odpowiednio go obsłuży.

Testujemy

Czas to teraz przetestować:

describe('Search for the car', function(){
    it('should return a result', function(){
        var d = $.Deferred();
        d.resolve('Opel');

        spyOn($, 'ajax').and.returnValue(d.promise());

        app.carsService.search();
        expect(app.carsService.car).toBe('Opel');
    });
});

W zaznaczonych liniach widzimy, w jaki sposób można “zamockować” wywołanie funkcji ajax. Na początku tworzony jest obiekt Defferd, który generalnie pozwala na opóźnione wywołanie jakiejkolwiek funkcji - nie będę się tutaj wdawać w szczegóły tego obiektu ponieważ, to temat na osobny wpis. Wyjaśnię tylko co dokładnie się tutaj dzieje: w drugiej zaznaczonej linii definiujemy wartość jaka zostanie przekazana do funkcji wywołania zwrotnego w przypadku powodzenia operacji (jest to ta wartość przekazywana do funkcji done); w kolejnej linii widzimy z kolei znaną już nam definicję “szpiega” - szpiegować będziemy właśnie funkcję ajax, przekazując jej jako zwracaną wartość “obietnicę” obiektu Deffered. W opisywanym przykładzie użyto jedynie funkcji resolve ponieważ, carsService obsługuje tylko funkcję done - jeśli chcielibyśmy przetestować obsługę funkcji fail, należałoby użyć funkcji reject obiektu Deffered. Hmm… mam nadzieję, że jasno to wytłumaczyłem :D

Co dalej? Ano wywołana jest funkcja search z modułu carsService, a następnie testowane jest czy rzeczywiście zwracana jest “zamokowana” wartość. Prawda, że proste?

Podsumowanie

Uff, testowanie operacji asynchronicznych mamy już obcykane! Tym samym dobrnęliśmy też do końca… całego cyklu “Testowanie z Jasmine”. Mam nadzieję, że przekonaliście się, że nie jest to wcale takie skomplikowane jak by się mogło wydawać. Zresztą każdy dobry programista wie jak ważne są testy jednostkowe, nie warto więc ich pomijać po stronie klienta szczególnie, że jak widać są do tego celu stworzone odpowiednie narzędzia!

P.S. Wpis ten jest częścią mini-serii na temat testowania Jasmine. Poniżej wszystkie części tej serii: