Czy wiesz, że jesteśmy również na Slacku? Dołącz do nas już teraz klikając tutaj!

Cardinality, Matcher & Action, czyli znowu GoogleMock! :)


2020-09-30, 02:30

Trzy pojęcia warte wyjaśnienia…

A my dalej lecimy z tematem testowania! :) W poprzednim wpisie omówiliśmy sobie różnice między oczekiwaniami oraz zachowaniami. Jednak, a móc dobrze testować, musimy poznać wszystkie mechanizmy - dowiedzieć się, czym są i kiedy się przydają. Przyjrzyjmy się głównemu zarysowi makra EXPECT_CALL, zaczerpniętego z dokumentacji GoogleTest:

EXPECT_CALL(mock-object, method (matchers)?)
     .With(multi-argument-matcher)     ?
     .Times(cardinality)               ?
     .InSequence(sequences)            *
     .After(expectations)              *
     .WillOnce(action)                 *
     .WillRepeatedly(action)           ?
     .RetiresOnSaturation();           ?

Mamy tutaj kilka pojęć, których znaczenie dobrze jest znać. W dzisiejszym wpisie podejmiemy próbę wyjaśnienia trzech podstawowych: Cardinality, Matcher oraz Action.

Najprostsze na początek: cardinality

Definiując oczekiwanie, możemy zdefiniować liczbę, ile razy oczekujemy, że zostanie wywołana nasza metoda-mock w oparciu o dopasowane parametry. Od tego służy metoda Times(...), której wywołanie może wyglądać na przykład tak:

EXPECT_CALL(mockObject, getValue())
     .Times(2)
     .WillOnce(Return(0));

Kiedy liczba wywołań mockowanej metody nie jest zgodna ze zdefiniowanymi przez nas oczekiwaniami, GoogleMock informuje nas o tym za pomocą odpowiedniego błędu, na przykład takiego:

Actual function call count doesn't match EXPECT_CALL(*mockObject, getValue())...
         Expected: to be called 5 times
         Actual: called twice - unsatisfied and active

Liczebności dostępne w GoogleMock

GoogleMock daje nam wyczerpującą listę dostępnych cardinalities:

  • AnyNumber() - oznacza, że nie interesuje nas liczba wywołań mockowanej metody
  • Exactly(N) - oznacza, że oczekiwać będziemy N wywołań mockowanej metody
  • AtLeast(N) - dajemy znać, że oczekujemy na conajmniej N wywołań mockowanej metody
  • AtMost(N) - oczekujemy co najwyżej N wywołań mockowanej metody
  • Between(M, N) - oczekujemy, że mockowana metoda zostanie wywołana w liczbie między M oraz N.

Dodatkowo, wspomnę, że Times(2) oznacza to samo, co Times(Exactly(2)) :)

Domyślne wartości

Ostatnim podpunktem dotyczącym metody Times(...) są wartości domyślne, które są zakładane w momencie, kiedy pominiemy wywołanie tej metody. Zgodnie z dokumentacją GoogleMock, mamy trzy możliwości:

  • Kiedy nie korzystamy z WillOnce() oraz WillRepeatedly(), automatycznie dopasowane zostaje Times(1)
  • Kiedy N-razy skorzystamy z WillOnce(), ale wewnątrz oczekiwania nie będzie wywołania WillRepeatedly(), dopasowane zostanie Times(N).
  • Kiedy N-razy skorzystamy z WillOnce(), a nasze oczekiwanie będzie posiadało wywołanie WillRepeatedly(), dopasowane zostanie Times(AtLeast(N)).

Czym są matchery?

Drugim z ważniejszych typów elementów dostępnych w GoogleMock są matchery. Jak sama nazwa wskazuje, coś one dopasowują. Dokładniej rzecz biorąc, chodzi o dopasowywanie argumentów funkcji definiowanego przez nas mocka. Jak wcześniej wspomniałem, nasze mocki muszą być w pełni zdefiniowane przed uruchomieniem testowanego kodu. W momencie, kiedy na mocku zostaje wywołana metoda, GoogleMock porównuje jej wywołanie wraz ze wszystkimi zdefiniowanymi w mocku zachowaniami/oczekiwaniami. Dopasowana zostaje sama metoda po jej nazwie (pamiętajmy, że mock jest klasą dziedziczącą) oraz parametry. I właśnie do dopasowywania parametrów wykorzystywane są matchery. Kiedy już posiedliśmy wiedzę na temat procesu pracy GoogleMock, jesteśmy gotowi na to spłaszczenie:

Matcher to zwykła funkcja zwracająca wartość typu bool

Po tym przecudnym stwierdzeniu możemy przejść do kolejnej części wpisu :)

Matchery dostarczone przez GoogleMock

GoogleMock dostarcza bogaty zestaw wbudowanych matcherów. Poniżej zamieszczam listę tych najczęściej wykorzystywanych:

  • Popularne porównania: Eq, Ge, Gt, Le, Lt, Ne, Not
  • Wartości float: FloatEq, FloatNear
  • Wartości double: DoubleEq, DoubleNear
  • Ciągi znakowe: StrEq, StrNe, StrCaseEq, StrCaseNe, HasSubstr, StartsWith, EndsWith
  • Kontenery: Contains, Each, Key, ElementsAre, UnorderedElementsAreArray
  • Operacje na wskaźnikach/referencjach: IsNull, NotNull, Ref, Pointee
  • Wyrażenia regularne: MatchesRegex, ContainsRegex
  • Agregacje: AllOf, AnyOf, AllArgs

Gdzie możemy wykorzystać matcher?

Podstawowym wykorzystaniem matcherów jest tworzenie oczekiwań i zachowań. Zatem będziemy mogli z nich korzystać podczas korzystania z makr EXPECT_CALL oraz ON_CALL, na przykład:

EXPECT_CALL(*mockObject, getValue(Eq(1)));

Powyższy kodzik oznacza, że oczekujemy wywołania na mocku metody getValue() z parametrem, dla którego matcher Eq zwróci wartość true. Do matchera przekazujemy wartość 1, zatem oczekujemy wywołania getValue(1). Poniżej przykład dla matchera operującego na ciągach znakowych:

EXPECT_CALL(*mockObject, getValue(StartsWith("Hello")));

Pamiętajmy, że kiedy nasze oczekiwanie nie zostaje spełnione, GoogleTest poinformuje nas o tym odpowiednim komunikatem:

  Expected arg #0: starts with "Hello"
         Actual: "AAA World"
         Expected: to be called at least once
         Actual: never called - unsatisfied and active

Powyżej skorzystaliśmy z matcherów przyjmujących argumenty. Nie jest to jednak żadną regułą, bo istnieje kilka matcherów nieprzyjmujących żadnego parametru, na przykład:

EXPECT_CALL(*mockObject, getValue(NotNull()));

Powyższy skrawek kodu oznacza, że test zaświeci się na czerwono, kiedy zostanie wywołana metoda getValue(NULL) lub getValue(nullptr).

Matchowanie dowolnej wartości

Niejednokrotnie możemy zechcieć jawnie zaniechać dopasowywania konkretnego parametru. Nie każdy scenariusz testowy musi opierać się na dopasowaniu konkretnych wartości do parametrów. Czasami możemy zechcieć sterować jedynie wywołaniem mockowanej metody dla konkretnej wartości tylko jednego z wielu parametrów. Aby osiągnąć podobny efekt, powinniśmy skorzystać z matchera _:

EXPECT_CALL(*mockObject, getValue(_, true, _));

W powyższym przykładzie oczekujemy, że zostanie wywołana metoda getValue(...), która jako drugi parametr przyjmuje wartość true. Wartości pozostałych parametrów są nam obojętne.

Kombinacje matcherów

Bardzo ciekawym rozwiązaniem, na jakie pozwala nam GoogleMock jest możliwość kombinacji kilku matcherów. Możemy dzięki temu np. stworzyć zaprzeczenie:

EXPECT_CALL(*mockObject, getValue(Not(HasSubstr("Love Rust"))));

…albo zdefiniować nieco bardziej skomplikowany warunek:

EXPECT_CALL(*mockObject, getValue(
    AllOf(Gt(5), Lt(10))
);

W powyższym kodzie zawężamy zakres oczekiwanej wartości (AllOf to matcher odpowiadający operatorowi && w instrukcji warunkowej). Kiedy będziemy chcieli rozszerzyć zakres oczekiwanej wartości (przez zastosowanie odpowiednika operatora ||), skorzystamy z matchera AnyOf(...):

EXPECT_CALL(*mockObject, getValue(
    AnyOf(Gt(10), Lt(5))
);

Do tego mamy jeszcze zestaw matcherów: AllOfArray(...) oraz AnyOfArray(...). Są one bardzo podobne do AllOf(...) oraz AnyOf(...), z tą różnicą, że zamiast przekazywać do nich matchery w miejscu, możemy przekazać listę inicjalizacyjną, kontener typowy dla STL, iterator bądź tablicę C-style. Zgodnie z dokumentacją GoogleMock, mamy do dyspozycji takie oto sygnatury:

AllOfArray({m0, m1, ..., mn})
AllOfArray(a_container)
AllOfArray(begin, end)
AllOfArray(array)
AllOfArray(array, count)

Oczywiście, to samo dotyczy AnyOfArray(...):

AnyOfArray({m0, m1, ..., mn})
AnyOfArray(a_container)
AnyOfArray(begin, end)
AnyOfArray(array)
AnyOfArray(array, count)

Pora na Akcje!

Ostatnim z omawianych dzisiaj elementów GMock są akcje. Nie ukrywam, przyswajanie ich zajęło mi najdłużej spośród całej trójki. Niemniej jednak, po zrozumieniu roli, jaką one pełnią, koncept reprezentowany przez GoogleMock stał się dla mnie pełny.

O ile matchery kontrolują moment, w którym wywołanie mockowanej metody oznaczone zostaje jako oczekiwane, to akcje definiują, co wewnątrz tej metody się wydarzy. Najprostszym przykładem jest zwrócenie przez metodę wartości:

EXPECT_CALL(*mockObject, getValue())
            .WillOnce(Return("Ok"));

Powyższy kod oznacza, że oczekujemy od obiektu mockObject wywołania metody getValue(). Definiujemy, że kiedy ta zostaje wywołana, jednorazowo zwróci ona wartość "Ok". Kiedy chcemy zdefiniować kilka Akcji wywołanych wraz z kolejnymi wywołaniami mockowanej metody, robimy to tak:

EXPECT_CALL(*mockObject, getValue())
            .WillOnce(Return("Ok"))
            .WillOnce(Return("I'm not OK"));

Jeżeli chcielibyśmy określić, że określona akcja powinna zostać uruchomiona każdorazowo, to zamiast WillOnce powinniśmy skorzystać z WillRepeatedly:

EXPECT_CALL(*mockObject, getValue())
            .WillRepeatedly(Return("Ok"));

Dzięki temu nie musimy kontrolować, ile razy uruchomiona zostaje mockowana metoda, określając za to wartość, która zostanie zwrócona za każdym razem, kiedy mockowana metoda zostanie wywołana.

Możliwe jest również coś takiego:

EXPECT_CALL(*mockObject, getValue())
            .WillOnce(Return("Ok"))
            .WillOnce(Return("Ok too"))
            .WillRepeatedly(Return("I'm not OK");

Oznacza to, że z pierwszym wywołaniem metody getValue() zwrócona zostanie wartość "Ok", z drugim będzie to wartość "Ok too", a z każdym kolejnym wywołaniem zostanie zwrócona wartość "I'm not OK".

Akcje dostępne w GoogleMock

GoogleMock posiada bardzo bogaty i skatalogowany zestaw Akcji, który opiszemy sobie pokrótce. W detalach natomiast zostaną one opisane w kolejnym wpisie.

Akcje związane ze zwróceniem wartości

Podstawowywm typem akcji są te, które definiują, jaka wartość zostanie zwrócona przez metodę-mock. Do dyspozycji mamy następujące pozycje:

  • Return() - odpowiednik return; dla metod typu void
  • Return(value) - zwrotka określonej wartości dla metod innych niż typu void
  • ReturnArg<N>() - zwracamy argument określony jako N-ty parametr, niezależnie od jego wartości
  • ReturnNew<T>(a1, ..., ak) - zwracamy nowy obiekt typu T. Parametry a1..ak zostają przekazane do konstruktora klasy T
  • ReturnNull() - zwracamy wartość nullptr, jeżeli to możliwe. W przeciwnej sytuacji zwracana jest wartość NULL
  • ReturnPointee(ptr) - zwracamy wartość, na którą wskazuje zmienna ptr
  • ReturnRef(variable) - zwócona zostaje referencja do zmiennej variable
  • ReturnRefOfCopy(value) - zwraca referencję do kopii wartości podanej jako parametr. Czas życia skopiowanej wartości jest tak długi, co czas życia wywołanej akcji
  • ReturnRoundRobin({a1, ..., ak}) - z każdym wowłoaniem akcji zostanie zwrócona kolejna wartość podana jako parametr

Akcje modyfikujące

Kolejnym rodzajem akcji są te, które zezwalają na interakcję wywoływanej metody z otoczeniem:

  • Assign(&variable, value) - do zmiennej podanej jako referencja zostaje przypisana wartość value
  • DeleteArg<N>() - wywołany zostaje operator delete do N-tego parametru przekazanego do mockowej metody
  • SaveArg<N>(pointer) - pod adres, na który wskazuje wskaźnik pointer zostaje zapisana wartość N-tego parametru metody mockowej
  • SaveArgPointee<N>(pointer) - podobnie, co SaveArg, z tą różnicą, że zapisana zostaje wartość, na którą wskazuje N-ty parametr metody, który jest wskaźnikiem
  • SetArgReferee<N>(value) - przypisuje wartość value do N-tego parametru metody mockowej, który jest typu referencyjnego
  • SetArgPointee<N>(value) - przypisuje wartość value do N-tego parametru metody mockowej, który jest typu wskaźnikowego
  • SetArgumentPointee<N>(value) - odpowiednik SetArgPointee, oznaczony jako przestarzały. Zostanie usunięty z wersją 1.7.0.
  • SetArrayArgument<N>(first, last) - kopiuje wartości z podanego zakresu do tablicy podanej jako N-ty parametr. Tablica źródłowa może być zarówno wskaźnikiem, jak również iteratorem
  • SetErrnoAndReturn(error, value) - ustawia wartość globalnego pola errno na error, a sama metoda zwraca wartość value
  • Throw(exception) - rzuca wyjątek exception

Akcje operujące na funkcjach, lambdach i funktorach

Przedostatnią kategorią akcji, o której dzisiaj wspomnę są te, które pracują na wszystkim, co możemy wywołać - funkcjach, lambdach oraz funktorach:

  • Invoke(f) - wywołuje funkcję/funktor podany jako parametr f
  • InvokeWithoutArgs(f) - wywołuje funkcję/funktor podany jako parametr f bez żadnych parametrów
  • Invoke(object_pointer, &class::method), wywołuje metodę class::method w kontekscie obiektu object_pointer z wszystkimi parametrami przekazanymi do mockowanej metody
  • InvokeWithoutArgs(object_pointer, &class::method) - wywołuje metodę class::method w kontekscie obiektu object_pointer bez parametrów
  • InvokeArgument<N>(arg1, arg2, ..., argk) - wywołuje N-ty parametr przekazany do mockowej funkcji, który musi być funkcją lub funktorem

Akcje złożone

Ostatnią kategorią są akcje, które w dokumentacji są nazwane złożonymi. Należą do nich:

  • DoAll(a1, a2, ..., aN) - wykonuje wiele akcji jedna po drugiej, następnie zwracając wynik ostatniej. Akcje od a1 do aN-1 muszą zwracać typ void, a otrzymane przez nie argumenty będą wyłącznie do odczytu
  • WithArg<N>(a) - wykonuje akcję a wraz z przekazaniem N-tego parametru mockowej metody do niej
  • WithArgs<N1, N2, ..., Nk>(a) - podobnie, co WithArg, działa dla wielu parametrów
  • WithoutArgs(a) - wykonuje akcję a bez przekazywania do niej argumentów
  • IgnoreResult(a) - uruchamia akcję a, ignorując jej wynik. Akcja a nie może zwracać typu void

Krótko o N-tym parametrze

W wielu miejscach wspominałem o N-tych parametrach. Wspomnę tylko, że N oznacza numer parametru przekazanego do mockowej metody, licząc od 0.

Podsumowanie

Dzisiejszy wpis potraktować należy jako względnie krótkie wprowadzenie do mechanizmów, z którymi na codzień będziemy pracować wybierając GoogleMock. W następnym wpisie z tej serii poruszymy wspomniane zagadnienia w nieco bardziej praktyczny sposób.


Bibliografia



Marcin Kukliński

Zawodowo backend developer, hobbystycznie pasjonat języka C++. Po godzinach poszerza swoją wiedzę na takie tematy jak teorii kompilacji oraz budowa formatów plików. Jego marzeniem jest stworzyć swój własny język programowania.



Podobne wpisy


Pssst! Używamy Cookies. Poprzez używanie naszego serwisu zgadzasz się na odczytywanie i zapisywanie Cookies w swojej przeglądarce.
Polityka Prywatności