Witam w kolejnym już wpisie z serii “Testowanie z Jasmine”. Tak jak obiecałem ostatnio, tym razem zajmiemy się bardzo przydatnym elementem frameworka Jasmine. Mianowicie sprawdzimy jak wygląda testowanie mechanizmem spy w tym frameworku. Pozwólcie, że w dalszej części wpisu będę posługiwał się zamiennie terminem polskim (szpieg) i angielskim (spy). Generalnie, jak sama nazwa wskazuje, taki szpieg służy do szpiegowania… :) A konkretniej szpiegowania wywołań funkcji oraz przekazywanych do niej argumentów. Zobaczmy z czym to się je…

Podstawowe użycie

Myślę, że nie ma co przedłużać wstępu… Myślę, że testowanie mechanizmem spy najlepiej jest pokazać po prostu na przykładzie. Dlatego proponuję na początek przeanalizować taki przykład:

describe("A spy", function() {
    var car,
        wheel = null;

    beforeEach(function() {
        car = {
            changeWheel: function(newWheel) {
                wheel = newWheel;
            }
        };

        spyOn(car, 'changeWheel');

        car.changeWheel(123);
        car.changeWheel(1, 2);
    });

    it("tracks that function was called", function() {
        expect(car.changeWheel).toHaveBeenCalled();
    });

    it("tracks all the arguments of its calls", function() {
        expect(car.changeWheel).toHaveBeenCalledWith(123);
        expect(car.changeWheel).toHaveBeenCalledWith(1, 2);
    });

    it("stops all execution on a function", function() {
        expect(wheel).toBeNull();
    });
});

No dobra, to czas na omówienie szczegółów. Jak widać powyżej, mamy tutaj definicję zestawu testów, którą znacie już z poprzednich wpisów. Na początek zatrzymajmy się na funkcji beforeEach. Zawiera ona definicję obiektu car, a w niej z kolei znajduje się funkcja changeWheel. To co ona robi nie interesuje nas specjalnie, ciekawe natomiast jest to co znajduje się dalej. W zaznaczonej linii widzimy wywołanie funkcji spyOn, która jest częścią frameworka Jasmine. Służy ona do zdefiniowania pojedynczego szpiega - w przykładzie jako pierwszy parametr podajemy obiekt, w którym znajduje się funkcja. Drugi parametr to nazwa funkcji, którą chcemy szpiegować (w postaci ciągu znaków).

Od teraz każde wywołanie funkcji changeWheel będzie śledzone (w kolejnych liniach mamy dwa takie przykładowe wywołania). Przejdźmy dalej… w kolejnych liniach mamy już testy, w których możemy zobaczyć jak z tak zdefiniowanego szpiega możemy skorzystać. W linii 19 (zaznaczona), widzimy wykorzystanie “matchera” toHaveBeenCalled, który dokonuje sprawdzenia zgodnie ze swoją nazwą :) - czyli całe wyrażenie znaczy mniej więcej: “oczekuję, że funkcja car.changeWheel została wywołana”.

W kolejnym “spec’u” widzimy dwa wywołania kolejnego “matchera” - toHaveBeenCalledWith. Tak jak się na pewno domyślacie, oznacza on sprawdzenie czy szpiegowana funkcja została wywołana z podanymi parametrami.

Ostatni “spec” pokazuje nam jeszcze jedną istotną rzecz. Otóż, uwaga uwaga, jeśli jakaś funkcja jest poddana szpiegowaniu, to tak na prawdę nie jest ona wywoływana - w istocie wcześniejsze wywołanie funkcji spyOn, spowodowało utworzenie “stub’a” tej funkcji. Udowadnia to ten test - sprawdzane jest czy wartość ustawiana w funkcji jest rzeczywiście ustawiona po jej wywołaniu - jak widać nie jest. To wszystko dzieje się tylko na czas testu - po zakończeniu wykonywania zestawu testowego, szpiegi są usuwane.

Zmiana zachowania szpiega

Opisane powyżej domyślne działanie szpiega, tworzącego “stub’a” funkcji może zostać zmodyfikowane. Wystarczy podczas jego definicji wywołać łańcuchowo and.callThrough:

spyOn(car, 'changeWheel').and.callThrough();

W ten sposób, zamiast tworzyć “stub’a” nakazaliśmy szpiegowi aby, oprócz śledzenia wywołań funkcji, także wykonywać jej implementację.

Jeden test może wymagać obu zachować jednocześnie. To również jest możliwe w przypadku szpiegów w Jasmine. Wyobraźmy sobie sytuację, że wykorzystaliśmy powyższy przykład w teście, a zaraz potem, w tym samym zestawie testowym potrzebujemy wywołać funkcję changeWheel jeszcze raz, ale tym razem jako “stub”. Aby to zrobić wystarczy wywołać taki kod:

car.changeWheel.and.stub();

Od tego momentu, funkcja changeWheel znów jest traktowana jak “stub” i jej implementacja nie jest wywoływana.

Za pomocą takich łańcuchowych wywołań, możliwe jest również kilka innych rzeczy. Jedną z nich jest nakazanie aby każde wywołanie jakiejś funkcji zwracało jakąś konkretną wartość:

spyOn(car, "changeWheel").and.returnValue('success');

Kolejną ciekawą możliwością jest podmiana implementacji szpiegowanej funkcji! Na przykład, w poniższy sposób możemy nakazać Jasmine, aby w momencie wywołania szpiegowanej funkcji, zamiast niej wywołała funkcję zwracającą międzynarodowe słowo testowe :D :

spyOn(car, "changeWheel").and.callFake(function() {
    return 'dupa';
});

Na koniec, chyba “must have” czyli wymuszanie rzucania wyjątkiem:

spyOn(car, "changeWheel").and.throwError("!!!!");

Właściwość “calls”

Wszystkie wywołania szpiegowanych funkcji zapisywane są we właściwości “calls”. Właściwość ta, zawiera bardzo przydatny obiekt, posiadający szereg użytecznych funkcji. Dla przykładu:

expect(car.changeWheel.calls.count()).toEqual(2);

Powyżej wykorzystałem funkcję count właściwości calls do sprawdzenia czy wywołanie funkcji wystąpiło dokładnie dwa razy. Oczywiście to tylko przykład, a tych funkcji jest o wiele więcej. Poniżej ich lista:

  • any() - zwraca wartość boolowską w zależności od tego czy nastąpiło już jakiekolwiek wywołanie czy też nie
  • count() - opisane już w przykładzie powyżej
  • argsFor(index) - zwraca parametry wywołania o indeksie przekazanym w parametrze
  • allArgs() - zwraca argumenty wszystkich wywołań
  • all() - zwraca kontekst wywołania (this) oraz argumenty wszystkich wywołań
  • mostRecent() - jak wyżej tyle, że tylko dla najświeższego wywołania
  • first() - jak wyżej tyle, że dla pierwszego wywołania
  • reset() - czyści wszystkie śledzenia dla danego szpiega

Zachęcam też, do zajrzenia do przykładów zamieszczonych w dokumentacji technicznej - są na prawdę dobre.

Testowanie mechanizmem spy - podsumowanie

No i to w zasadzie tyle na dziś. Jak widzicie testowanie mechanizmem spy to nic trudnego. Mam nadzieję, że nie zanudzam Was zbytnio tymi wpisami o testowaniu… bo to jeszcze nie koniec :P W następnym wpisie zobaczymy jak testować asynchronicznie! Oczywiście z wykorzystaniem Jasmine :)

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