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

Definiowanie zachowań i oczekiwań w GoogleMock


2020-03-26, 02:45

Jak zdążyliście już zauważyć w poprzednich wpisach (np. Testy jednostkowe z GoogleTest, albo Podstawy pracy z GoogleMock), pisanie testów polega na definiowaniu oczekiwań względem kodu. O ile działając w GoogleTest oczekiwać będziemy, że odpowiednie miejsca w pamięci po wykonaniu funkcji będą miały określoną wartość (tak tak, wyjątki to też pewnego rodzaju wartość w pamięci), to pracując z GoogleMock będziemy definiowali swoje oczekiwania względem uruchamianego wewnątrz jednostki kodu. I właśnie o tym będzie dzisiejszy wpis, na który serdecznie zapraszam! :)

EXPECT_CALL vs ON_CALL

W pierwszej części serii o GoogleMock stworzyliśmy testy korzystające z makra EXPECT_CALL. Działa ono na podobnej zasadzie co makra z rodziny EXPECT_ znajdujące się w bibliotece Google Test. W pierwszym parametrze makra przekazujemy zamockowany obiekt, a w drugiej wywołanie metody, której dotyczy definiowane przez nas oczekiwanie. Takim oczekiwaniem może być na przykład wywołanie tej metody dokładnie jeden raz. W przypadku, kiedy ta metoda nie zostanie wywołana na obiekcie mockującym - test nie zostanie zaliczony.

Jedną różnicą w przeciwieństwie do GoogleTest jest to, że jeżeli makro EXPECT_CALL zostaje użyte, to musi zostać wykorzystane przed uruchomieniem tesowanego kodu. Czyli tak to będzie wyglądało w GoogleTest:

TEST(MyTest, test_playground) {
    MyClass myClass;
    auto value = myClass.doSomething(mockObject);

    EXPECT_EQ(value, "hello");
}

a tak w GoogleMock:

TEST(MyTest, test_playground) {
    MyClass myClass;
    auto mockObject = std::make_shared<MockClass>();

    EXPECT_CALL(*mockObject, doSomething())
        .Times(1)
        .WillOnce(Return("hello"));

    auto value = myClass.doSomething(mockObject);
}

Różnica pomiędzy kolejnością wykorzystania tych makr jest związana z mechamizmem działania obydwu bibliotek. GoogleTest uruchamia testowany kawałek kodu, nie ingerując w jego wykonanie. Interesuje go tylko to, co zostaje przez niego zwrócone (zwracana wartość i wyjątki). GoogleMock natomiast korzysta z obiektów-mocków, które działają już w trakcie trwania testowanej logiki. Do momentu jej uruchomienia musimy mieć dostępne informacje o wszystkich oczekiwaniach względem tego kodu.

Oprócz definiowania oczekiwań, makro EXPECT_CALL służy również do definiowania zachowań, czyli akcji, które mają być uruchomione w momencie napotkania odpowiedniego wywołania metody.

Makro ON_CALL

Przy temacie makra EXPECT_CALL bardzo często występuje skojarzenie z drugim makrem - ON_CALL. Pomimo, że obydwa te makra mają podobną konstrukcję, to jest jedna znacząca różnica między nimi. Makro EXPECT_CALL kojarzyć będziemy z ostatnim etapem wykonywania się testu - assercją. Jeżeli którekolwiek ze zdefiniowanych przez nas oczekiwań nie zostanie spełnione, test zostanie zakończony niepowodzeniem. Z makrem ON_CALL jest nieco inaczej. Tutaj tworzymy wyłącznie zachowania, które wcale nie muszą zostać spełnione. Jeżeli jednak na obiekcie mocka zostanie wywołana metoda z odpowiednimi parametrami, to zostanie wykonana zdefiniowana przez nas akcja.

Przyjżyjmy się przykładowi:

#include <gtest/gtest.h>
#include <gmock/gmock.h>

#include <memory>
#include <string>

using ::testing::Return;

class Value {

public:

    virtual std::string getValue() = 0;

};

class MyClass {

public:

    bool isValueOk(std::shared_ptr<Value> value) {
        return value->getValue() == "Ok";
    }

};

class ValueMock : public Value {

public:

    MOCK_METHOD0(getValue, std::string());

};

TEST(MyTest, test_the_value_is_ok) {
    MyClass myClass;
    auto mockObject = std::make_shared<ValueMock>();

    ON_CALL(*mockObject, getValue())
            .WillByDefault(
                Return("Ok")
            );

    auto value = myClass.isValueOk(mockObject);

    EXPECT_TRUE(value);
}

Trochę dużo się tutaj dzieje, ale po kolei. Mamy klasę Value, która dostarcza nam pewną wartość. Od tej wartości zależy wykonanie metody isValueOk(...) w klasie MyClass. Następnie tworzymy mock ValueMock, którego zachowania definiujemy przy użyciu makra ON_CALL. Naszym zachowaniem będzie zwracanie wartości "Ok". Kiedy mamy zdefiniowane zachowania naszego mocka, uruchamiamy testowaną logikę. W momencie, kiedy wewnątrz metody MyClass::isValueOk(...) zostanie wywołana linijka return value->getValue() == "Ok";, GoogleMock zajrzy do zdefiniowanych przez nas zachowań dla klasy mocka i na ich podstawie zdecyduje, jaką wartość zwrócić. Na samym końcu sprawdzamy, czy aby na pewno MyClass::isValueOk(...) zwróci prawdę.

Gdyby metoda MyClass::isValueOk(...) wyglądała następująco:

    bool isValueOk(std::shared_ptr<Value> value) {
        return true;
    }

To test i tak zostanie zakończony sukcesem.

Przestudiujmy w takim razie przykład z EXPECT_CALL:

#include <gtest/gtest.h>
#include <gmock/gmock.h>

#include <memory>
#include <string>

using ::testing::Return;

class Value {

public:

    virtual std::string getValue() = 0;

};

class MyClass {

public:

    bool isValueOk(std::shared_ptr<Value> value) {
        return value->getValue() == "Ok";
    }

};

class ValueMock : public Value {

public:

    MOCK_METHOD0(getValue, std::string());

};

TEST(MyTest, test_the_value_is_ok) {
    MyClass myClass;
    auto mockObject = std::make_shared<ValueMock>();

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

    auto value = myClass.isValueOk(mockObject);

    EXPECT_TRUE(value);
}

Podobnie, jak w poprzednim przykładzie - tworzymy zachowanie, które jednocześnie jest oczekiwaniem. Tutaj wręcz wymagamy, aby metoda Value::getValue() została wywołana. Jeżeli zostanie wywołana zgodnie z naszą definicją, zwróci wartość "Ok". Jeżeli jednak nie zarejestrujemy wykonania tej metody, to nasz test zostanie zakończony niepowodzeniem.

Przegląd API EXPECT_CALL i ON_CALL

Pomimo, że na pierwszy rzut oka obydwa wspominane dzisiaj makra wyglądają do siebie bardzo podobnie, to są to dwa fizycznie różne byty dla GoogleMock. Różnią się one nie tylko swoim przeznaczeniem, ale również API, czyli listą metod, z których możemy skorzystać podczas tworzenia definicji.

API ON_CALL

Do tej chwili wystarczyło nam stwierdzenie, że makro ON_CALL służy do definiowania zachowań. Jest to półprawda, ponieważ ON_CALL służy wyłącznie do definiowania domyślnych akcji dla wywołanej metody. I w zasadzie tylko na to zezwala nam owo makro. Zobaczcie sami:

ON_CALL(*mockObject, getValue())
            .With(/* MATCHER */)
            .WillByDefault(/* ACTION */);

Do dyspozycji mamy tylko dwie metody: With(...) oraz WillByDefault(...). Pierwsza metoda służy do zdefiniowania argumentów, z którymi oczekujemy, że metoda zostanie wywołana. Druga metoda służy do zdefiniowania akcji domyślnej, wykonywanej w chwili dopasowania bieżącego wywołania do definicji. Na tą chwilę możemy przyjąć, że w to miejsce możemy wrzucić informację o wartości zwracanej przez mockowaną metodę.

API EXPECT_CALL

Makro EXPECT_CALL daje nam nieco więcej zabawek. Przyjrzyjmy się metodom, które możemy wywołać podczas definiowania oczekiwań:

EXPECT_CALL(*mockObject, getValue())
     .With(/* MATCHER */)
     .Times(/* INTEGER */)
     .InSequence(/* SEQUENCES */)
     .After(/* EXPECTATIONS */)
     .WillOnce(/* ACTION */)
     .WillRepeatedly(/* ACTION */)
     .RetiresOnSaturation();

Pozwolę sobie omówić każdą z tych metod w sposób zwięzły. W przyszłości będziemy poruszać każdą z nich nieco szerzej.

Metoda With(...) służy do definiowania argumentów, z którymi oczekujemy, że metoda będzie wywołana. Możemy oczekiwać, że konkretna metoda z tymi samymi argumentami może zostać wywołana określoną ilość razy - do tego przyda nam się metoda Times(...). Domyślnie GoogleMock nie dba o kolejność wywoływania mockowanych metod. Jeżeli chcemy zarządzać kolejnością ich wowyływania, skorzystamy z metody InSequence(...). Następne dwie metody: WillOnce(...) oraz WillRepeatedly(...) służą do zdefiniowania akcji, która zostanie uruchomiona jednorazowo, bądź za każdym razem, kiedy zostanie dopasowana definicja naszego oczekiwania. Kiedy skorzystamy z metody RetiresOnSaturation(), przekażemy GoogleMock, że jeżeli nasze oczekiwanie zostanie całkowicie spełnione, to chcemy, aby było pomijane przy następnych przeszukiwaniach zdefiniowanych przez nas oczekiwań.

Kolejność definiowania zachowań i oczekiwań

Podczas pisania testów będziemy definiowali conajmniej kilka różnych oczekiwań/zachowań jednocześnie. Dobrze jest znać sposób, w jaki GoogleMock przegląda nasze definicje, aby móc dobrze napisać test.

Oczekiwania kontra zachowania domyślne

Skoro wiemy już, że makro ON_CALL służy do definiowania wyłącznie zachowań domyślnych, to na pewno będzie miało ono mniejszy priorytet podczas przeglądania definicji niż EXPECT_CALL. Przeanalizujmy w takim razie następujący scenariusz:

TEST(MyTest, test_order_for_default_behaviour) {
    MyClass myClass;
    auto mockObject = std::make_shared<ValueMock>();

    ON_CALL(*mockObject, getValue())
            .WillByDefault(
                    Return("NotOk")
            );

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

    auto value = myClass.isValueOk(mockObject);

    EXPECT_TRUE(value);
}

Zdefiniowaliśmy domyślne zachowanie dla metody getValue() należącej do obiektu-mocka. Zawiera ono zachowanie, które spowodowałoby, że ostatnia linijka testu zakończyłaby go niepowodzeniem. Następnie zdefiniowaliśmy oczekiwanie dla dokładnie takiego samego wywołania metody getValue(). Tutaj jednak definiujemy zachowanie, które pozwoliłoby na pomyślne zakończenie testu. Jak sądzicie, jaki będzie wynik tego testu?

Odpowiedź brzmi:

[==========] Running 1 test from 1 test case.
[----------] Global test environment set-up.
[----------] 1 test from MyTest
[ RUN      ] MyTest.test_order_for_default_behaviour
[       OK ] MyTest.test_order_for_default_behaviour (1 ms)
[----------] 1 test from MyTest (1 ms total)

[----------] Global test environment tear-down
[==========] 1 test from 1 test case ran. (1 ms total)
[  PASSED  ] 1 test.

Zatem prawdziwym staje się stwierdzenie, że zachowania zdefiniowane w oczekiwaniach mają większy priorytet niż zachowania domyślne.

Czy aby jednak na pewno tak jest? Co, jeżeli dorzucimy nowe zachowanie domyślne, ale zaraz po definicji naszego oczekiwania? O, tak:

TEST(MyTest, test_order_for_default_behaviour) {
    MyClass myClass;
    auto mockObject = std::make_shared<ValueMock>();

    ON_CALL(*mockObject, getValue())
            .WillByDefault(
                    Return("NotOk")
            );

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

    ON_CALL(*mockObject, getValue())
            .WillByDefault(
                    Return("StillNotOk")
            );

    auto value = myClass.isValueOk(mockObject);

    EXPECT_TRUE(value);
}

W sumie, to nasze nowe zachowanie domyślne mogłoby przecież przykryć zdefiniowane wcześniej oczekiwanie. Wypada w takim razie sprawdzić i ten scenariusz:

[==========] Running 1 test from 1 test case.
[----------] Global test environment set-up.
[----------] 1 test from MyTest
[ RUN      ] MyTest.test_order_for_default_behaviour
[       OK ] MyTest.test_order_for_default_behaviour (1 ms)
[----------] 1 test from MyTest (1 ms total)

[----------] Global test environment tear-down
[==========] 1 test from 1 test case ran. (1 ms total)
[  PASSED  ] 1 test.

Nic bardziej mylnego. Zachowanie domyślne w dalszym ciągu jest zachowaniem domyślnym i nie ma ono priorytetu przed oczekiwaniem identycznego wywołania metody, nawet jeżeli zostało ono zdefiniowane w późniejszej kolejności.

Kolejność zachowań domyślnych

W porządku, co w takim razie w sytuacji, kiedy mamy dwa zachowania domyślne? Które jest ważniejsze? Oczywiście, kodzik poniżej:

TEST(MyTest, test_order_for_default_behaviour) {
    MyClass myClass;
    auto mockObject = std::make_shared<ValueMock>();

    ON_CALL(*mockObject, getValue())
            .WillByDefault(
                    Return("NotOk")
            );

    ON_CALL(*mockObject, getValue())
            .WillByDefault(
                    Return("Ok")
            );

    auto value = myClass.isValueOk(mockObject);

    EXPECT_TRUE(value);
}

Mamy identyczne wywołania metody, definiujemy dwa zachowania domyślne. Jeżeli nie znamy zasady, to mamy 50% szans na trafienie. Jak tam, znacie odpowiedź? Jeżeli nie, to lecimy z logiem wykonania testu:

[==========] Running 1 test from 1 test case.
[----------] Global test environment set-up.
[----------] 1 test from MyTest
[ RUN      ] MyTest.test_order_for_default_behaviour

GMOCK WARNING:
Uninteresting mock function call - taking default action specified at:
/Users/senghe/Desktop/definiowanie-zachowan-i-oczekiwan-w-googlemock/tests/Test/MyTest.cpp:44:
    Function call: getValue()
          Returns: "Ok"
NOTE: You can safely ignore the above warning unless this call should not happen.  Do not suppress it by blindly adding an EXPECT_CALL() if you don't mean to enforce the call.  See https://github.com/google/googletest/blob/master/googlemock/docs/CookBook.md#knowing-when-to-expect for details.
[       OK ] MyTest.test_order_for_default_behaviour (0 ms)
[----------] 1 test from MyTest (0 ms total)

[----------] Global test environment tear-down
[==========] 1 test from 1 test case ran. (0 ms total)
[  PASSED  ] 1 test.

Pierwsze, co rzuca nam się w oczy, to komunikat o początku NOTE:. Jest to dla nas informacja, że zdefiniowaliśmy domyślne zachowania, ale nie zdefiniowaliśmy żadnych oczekiwań. Teoretycznie mogliśmy chcieć właśnie w ten sposób działać, dlatego jest to zwykła informacja, a nie błąd.

Pomijając ten komentarz, zauważymy, że test wykonał się pomyślnie. Jest tak, ponieważ GoogleMock przegląda zdefiniowane zachowania (w tej chwili mówimy o zachowaniach domyślnych) w kolejności odwrotnej do kolejności definicji. Jeżeli napotka pierwszą definicję, a wywołanie metody będzie się z nią zgadzało - zostanie ona wybrana.

Ok, sprawdźmy w takim razie, jak sprawa ma się z zachowaniami związanymi z oczekiwaniami.

Kolejność wywoływania oczekiwań

Bardzo ciekawe rzeczy mają się do zdublowanych oczekiwań względem wykonywanego kodu:

TEST(MyTest, test_order_for_expected_behavious) {
    MyClass myClass;
    auto mockObject = std::make_shared<ValueMock>();

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

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

    auto value = myClass.isValueOk(mockObject);

    EXPECT_TRUE(value);
}

Tworzymy dwa oczekiwania dla identycznego wywołania metody getValue(). Pierwsze z nich definiuje zachowanie, które sprawi, że assercja EXPECT_TRUE(value); będzie zachowana. Drugie z nich definiuje zachowanie z wartością, które wpłynie negatywnie na tą assercję.

Pytanie do Was: jak sądzicie, które zachowanie zostanie wybrane przez GoogleMock? :)

Gotowi na odpowiedź? Łapcie! :)

[==========] Running 1 test from 1 test case.
[----------] Global test environment set-up.
[----------] 1 test from MyTest
[ RUN      ] MyTest.test_order_for_expected_behavious
/Users/senghe/Desktop/definiowanie-zachowan-i-oczekiwan-w-googlemock/tests/Test/MyTest.cpp:51: Failure
Value of: value
  Actual: false
Expected: true
/Users/senghe/Desktop/definiowanie-zachowan-i-oczekiwan-w-googlemock/tests/Test/MyTest.cpp:39: Failure
Actual function call count doesn't match EXPECT_CALL(*mockObject, getValue())...
         Expected: to be called once
           Actual: never called - unsatisfied and active
[  FAILED  ] MyTest.test_order_for_expected_behavious (0 ms)
[----------] 1 test from MyTest (0 ms total)

[----------] Global test environment tear-down
[==========] 1 test from 1 test case ran. (0 ms total)
[  PASSED  ] 0 tests.
[  FAILED  ] 1 test, listed below:
[  FAILED  ] MyTest.test_order_for_expected_behavious

 1 FAILED TEST

Już na oko widać, że test nie powiódł się. GoogleTest raportuje nam dwa problemy.

Pierwszy z nich oznacza, że liczba zdefiniowanych oczekiwań związanych z makrem EXPECT_CALL nie zgadza się z faktycznym stanem uruchomieniowym. Wygląda w takim razie na to, że każde zdefiniowane oczekiwanie musi zostać spełnione. Czyli w tym wypadku nasza metoda getValue() powinna zostać wywołana dwukrotnie. I w sumie jest to dosyć logiczne podejście.

Drugim problem zgłoszonym przez GoogleTest jest niespełnienie oczekiwania związanego z makrem EXPECT_TRUE(value);. Skoro mieliśmy zdefiniowane dwa zachowania - jedno, które pozytywnie wpłynęłoby na tą assercję, a drugie - negatywnie, to wygląda na to, że GoogleMock przeszukuje zdefiniowane zachowania w kierunku - podobnie jak zachowania domyślne - odwrotnym do kolejności definiowania.

Aby potwierdzić ostatecznie tą tezę, spróbujmy odwrócić kolejność definicji naszych oczekiwań:

TEST(MyTest, test_order_for_expected_behavious) {
    MyClass myClass;
    auto mockObject = std::make_shared<ValueMock>();

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

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

    auto value = myClass.isValueOk(mockObject);

    EXPECT_TRUE(value);
}

Wynik testu po zmianie kolejności wygląda następująco:

[==========] Running 1 test from 1 test case.
[----------] Global test environment set-up.
[----------] 1 test from MyTest
[ RUN      ] MyTest.test_order_for_expected_behavious
/Users/senghe/Desktop/definiowanie-zachowan-i-oczekiwan-w-googlemock/tests/Test/MyTest.cpp:39: Failure
Actual function call count doesn't match EXPECT_CALL(*mockObject, getValue())...
         Expected: to be called once
           Actual: never called - unsatisfied and active
[  FAILED  ] MyTest.test_order_for_expected_behavious (0 ms)
[----------] 1 test from MyTest (0 ms total)

[----------] Global test environment tear-down
[==========] 1 test from 1 test case ran. (0 ms total)
[  PASSED  ] 0 tests.
[  FAILED  ] 1 test, listed below:
[  FAILED  ] MyTest.test_order_for_expected_behavious

 1 FAILED TEST

Wygląda na to, że nasza teza została potwierdzona - tym razem GoogleMock również wybrał zachowanie zdefiniowane jako ostatnie.
Przy okazji widzimy, że dla GoogleMock nie jest ważna kolejność definicji oczekiwań (przynamniej domyślnie).

Podsumujmy sobie w takim razie zebraną wiedzę w jednym akapicie.

GoogleMock - to jak to w końcu jest?

Powyższe eksperymenty możemy zakończyć trzema prostymi wnioskami:

  1. W momencie uruchomienia GoogleMock przegląda zdefiniowane zachowania w kolejności odwrotnej od kolejności ich definiowania.
  2. Zachowania zdefiniowane przez makro EXPECT_CALL mają priorytet nad zachowaniami zdefiniowanymi przez makro ON_CALL.
  3. Wszystkie zdefiniowane przez nas oczekiwania muszą zostać spełnione. Domyślnie kolejność definiowania oczekiwań nie ma żadnego znaczenia.

Podsumowanie

W tym wpisie dowiedzieliśmy się, czym dla GoogleMock są zachowania oraz oczekiwania. Poznaliśmy istniejącą pomiędzy nimi różnicę. Dodatkowo zrobiliśmy kilka eksperymentów, dzieki którym poznaliśmy sposób, w który GoogleMock przeszukuje bazę zdefiniowanych zachowań. Dowiedzieliśmy się również, w jaki sposób GoogleMock traktuje kilka oczekiwań zdefiniowanych dla takiego samego wywołania metody z obiektu-mocka.



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.

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