Cardinality, Matcher & Action, czyli znowu GoogleMock! :)
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 metodyExactly(N)
- oznacza, że oczekiwać będziemy N wywołań mockowanej metodyAtLeast(N)
- dajemy znać, że oczekujemy na conajmniej N wywołań mockowanej metodyAtMost(N)
- oczekujemy co najwyżej N wywołań mockowanej metodyBetween(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()
orazWillRepeatedly()
, automatycznie dopasowane zostajeTimes(1)
- Kiedy N-razy skorzystamy z
WillOnce()
, ale wewnątrz oczekiwania nie będzie wywołaniaWillRepeatedly()
, dopasowane zostanieTimes(N)
. - Kiedy N-razy skorzystamy z
WillOnce()
, a nasze oczekiwanie będzie posiadało wywołanieWillRepeatedly()
, dopasowane zostanieTimes(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()
- odpowiednikreturn;
dla metod typuvoid
Return(value)
- zwrotka określonej wartości dla metod innych niż typuvoid
ReturnArg<N>()
- zwracamy argument określony jako N-ty parametr, niezależnie od jego wartościReturnNew<T>(a1, ..., ak)
- zwracamy nowy obiekt typuT
. Parametry a1..ak zostają przekazane do konstruktora klasyT
ReturnNull()
- zwracamy wartośćnullptr
, jeżeli to możliwe. W przeciwnej sytuacji zwracana jest wartośćNULL
ReturnPointee(ptr)
- zwracamy wartość, na którą wskazuje zmiennaptr
ReturnRef(variable)
- zwócona zostaje referencja do zmiennejvariable
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 akcjiReturnRoundRobin({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 metodySaveArg<N>(pointer)
- pod adres, na który wskazuje wskaźnikpointer
zostaje zapisana wartość N-tego parametru metody mockowejSaveArgPointee<N>(pointer)
- podobnie, coSaveArg
, z tą różnicą, że zapisana zostaje wartość, na którą wskazuje N-ty parametr metody, który jest wskaźnikiemSetArgReferee<N>(value)
- przypisuje wartośćvalue
do N-tego parametru metody mockowej, który jest typu referencyjnegoSetArgPointee<N>(value)
- przypisuje wartośćvalue
do N-tego parametru metody mockowej, który jest typu wskaźnikowegoSetArgumentPointee<N>(value)
- odpowiednikSetArgPointee
, 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ż iteratoremSetErrnoAndReturn(error, value)
- ustawia wartość globalnego polaerrno
naerror
, a sama metoda zwraca wartośćvalue
Throw(exception)
- rzuca wyjątekexception
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 parametrf
InvokeWithoutArgs(f)
- wywołuje funkcję/funktor podany jako parametrf
bez żadnych parametrówInvoke(object_pointer, &class::method)
, wywołuje metodęclass::method
w kontekscie obiektuobject_pointer
z wszystkimi parametrami przekazanymi do mockowanej metodyInvokeWithoutArgs(object_pointer, &class::method)
- wywołuje metodęclass::method
w kontekscie obiektuobject_pointer
bez parametrówInvokeArgument<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 oda1
doaN-1
muszą zwracać typvoid
, a otrzymane przez nie argumenty będą wyłącznie do odczytuWithArg<N>(a)
- wykonuje akcjęa
wraz z przekazaniem N-tego parametru mockowej metody do niejWithArgs<N1, N2, ..., Nk>(a)
- podobnie, coWithArg
, działa dla wielu parametrówWithoutArgs(a)
- wykonuje akcjęa
bez przekazywania do niej argumentówIgnoreResult(a)
- uruchamia akcjęa
, ignorując jej wynik. Akcjaa
nie może zwracać typuvoid
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.