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

Metoda wyszukiwania testów przez GoogleTest


2019-12-12, 00:00

Biblioteka GoogleTest uwiodła mnie swoją prostotą użytkowania. Praca z nią polega głównie na stworzeniu pliku z funkcją main oraz utworzeniu plików zawierających testy. Bez zbędnego rejestrowania testów, bez zbędnej konfiguracji. Jak przyjrzymy się nieco bliżej, to możemy zadać sobie pytanie: skąd GoogleTest “wie” o napisanych przez nas testach? W dzisiejszym wpisie odpowiemy sobie na to pytanie.

Kod obrazujący problem

Dzisiejszy problem najlepiej jest przedstawić na przykładzie. Wyobraźmy sobie, że w projekcie mamy dwa pliki:

Plik TestCase.cpp

#include <gtest/gtest.h>

TEST(TestCaseName, test_name) {
    EXPECT_TRUE(true);
}

Plik main.cpp

#include <gtest/gtest.h>

int main(int argc, char *argv[]) {
    ::testing::InitGoogleTest(&argc, argv);
    return RUN_ALL_TESTS();
}

Jak nam dobrze wiadomo z mojego wpisu Wstęp do teorii kompilacji, mamy tu do czynienia z dwoma odseparowanymi od siebie jednostkami kompilacji. Nie widać tutaj nic, co by mogło wskazywać na jakąkolwiek relację łączącą te jednostki. No, może oprócz nagłówka #include <gtest/gtest.h>. Czy macie pomysł, w jaki sposób mogą być powiązane obydwa te pliki? Zastanówcie się chwilę, może sami na to wpadniecie ;)

Wszystkiemu winien preprocesor

Pierwszym krokiem, który dużo nam wyjaśni jest pełne rozwinięcie makra TEST. Zatem po kolei :)

Jak widzimy poniżej, mamy tutaj drobne proxy w preprocesorze, które jest nie jest bez przyczyny. Zwyczajem bibliotek testujących stało się korzystanie z makra TEST z parametrami test_case_name oraz test_name. Biblioteka GoogleTest daje nam szybką możliwość przepięcia bez większego przepisywania testów.

Jeżeli chcemy, to możemy jawnie korzystać z makra GTEST_TEST. Jak widać, to jest to samo.

# define TEST(test_case_name, test_name) GTEST_TEST(test_case_name, test_name)

Definicja makra GTEST_TEST wygląda następująco:

#define GTEST_TEST(test_case_name, test_name)\
  GTEST_TEST_(test_case_name, test_name, \
              ::testing::Test, ::testing::internal::GetTestTypeId())

Zaczyna robić się ciekawie. Makro GTEST_TEST_ jest makrem generycznym, z którego korzysta makro TEST_F. Różnicą pomiędzy tymi dwoma wywołaniami są dwa ostatnie parametry.

Lecimy dalej z rozwiązywaniem makr. W następnym kroku będziemy potrzebowali wartości dla dwóch pierwszych parametrów tego makra. Przyjmijmy, że test_case_name będzie miało wartość MyTestCase, a test_name przyjmie wartość my_test.

Przyjrzyjmy się w takim razie definicji makra GTEST_TEST_:

#define GTEST_TEST_(test_case_name, test_name, parent_class, parent_id)\
class GTEST_TEST_CLASS_NAME_(test_case_name, test_name) : public parent_class {\
 public:\
  GTEST_TEST_CLASS_NAME_(test_case_name, test_name)() {}\
 private:\
  virtual void TestBody();\
  static ::testing::TestInfo* const test_info_ GTEST_ATTRIBUTE_UNUSED_;\
  GTEST_DISALLOW_COPY_AND_ASSIGN_(\
      GTEST_TEST_CLASS_NAME_(test_case_name, test_name));\
};\
\
::testing::TestInfo* const GTEST_TEST_CLASS_NAME_(test_case_name, test_name)\
  ::test_info_ =\
    ::testing::internal::MakeAndRegisterTestInfo(\
        #test_case_name, #test_name, NULL, NULL, \
        ::testing::internal::CodeLocation(__FILE__, __LINE__), \
        (parent_id), \
        parent_class::SetUpTestCase, \
        parent_class::TearDownTestCase, \
        new ::testing::internal::TestFactoryImpl<\
            GTEST_TEST_CLASS_NAME_(test_case_name, test_name)>);\
void GTEST_TEST_CLASS_NAME_(test_case_name, test_name)::TestBody()

Ło matko - pomyślałem, jak pierwszy raz to zobaczyłem. Jak widzimy, sporo tutaj preprocesora. Nawet skrypt do listingów nie ogarnia, co tu się dzieje ;) Rozwiążmy sobie w takim razie to makro już do końca:

class MyTestCase_my_test_Test : public ::testing::Test {

public:

    MyTestCase_my_test_Test() {}

private:

    virtual void TestBody();
    static ::testing::TestInfo* const test_info_ __attribute__ ((unused));
    MyTestCase_my_test_Test(MyTestCase_my_test_Test const &);
    void operator=(MyTestCase_my_test_Test const &);
};

::testing::TestInfo* const MyTestCase_my_test_Test::test_info_ = ::testing::internal::MakeAndRegisterTestInfo(
    "MyTestCase",
    "my_test",
    NULL,
    NULL,
    ::testing::internal::CodeLocation(__FILE__, __LINE__),
    (::testing::internal::GetTestTypeId()),
    parent_class::SetUpTestCase,
    parent_class::TearDownTestCase,
    new ::testing::internal::TestFactoryImpl<MyTestCase_my_test_Test>
);

void MyTestCase_my_test_Test::TestBody()

// W tym miejscu pojawiają się klamerki { } zdefiniowanego przez nas testu.

O wiele lepiej, nie prawda? Teraz możemy przyjrzeć się nieco bliżej temu skrawkowi kodu :)

Powyższy kawałek kodu możemy podzielić na trzy części. Pierwsza z nich to definicja klasy reprezentującej nasz test. Wykorzystywana tutaj jest wewnętrzna implementacja GoogleTest. Drugi krok to metoda rejestrująca “gdzieś” świeżo zdefiniowaną klasę testu. Trzecia część to definicja testowanej logiki.

Poszukujemy miejsca rejestracji testu, dlatego przejdziemy do analizy kroku drugiego. Spójrzmy zatem na definicję funkcji MakeAndRegisterTestInfo:

TestInfo* MakeAndRegisterTestInfo(
    const char* test_case_name,
    const char* name,
    const char* type_param,
    const char* value_param,
    CodeLocation code_location,
    TypeId fixture_class_id,
    SetUpTestCaseFunc set_up_tc,
    TearDownTestCaseFunc tear_down_tc,
    TestFactoryBase* factory
) {
    TestInfo* const test_info = new TestInfo(
        test_case_name, name, type_param, value_param, code_location, fixture_class_id, factory
    );

    GetUnitTestImpl()->AddTestInfo(set_up_tc, tear_down_tc, test_info);
    return test_info;
}

Jesteśmy tylko o krok rozwiązania naszej zagadki! Mamy funkcję, która zwraca nam byt, do którego dorzucamy kolejne testy. Pozostaje nam już tylko sprawdzić, co kryje się wewnątrz funkcji GetUnitTestImpl():

inline UnitTestImpl* GetUnitTestImpl() {
    return UnitTest::GetInstance()->impl();
}

Ten zapis powinien być każdemu znany, więc nie ma chyba sensu iść głębiej :) Tak tak, to… singleton! Wychodzi na to, że wszystkie testy rejestrowane są wewnątrz jednej instancji. W sumie to spoko, ale raczej nie za dobrze…

Singleton, tak bardzo znienawidzony przez programistów

Celowo moje poprzednie zdanie zachowuje podwójny wydźwięk. Wzorzez Singleton jest dosyć kontrowersyjnym wzorcem. Bardzo często można spotkaś się z nazwą “Antywzorzec Singleton”. Czy zastanawialiście się, dlaczego tak jest? Mam własne przemyślenia na ten temat, którymi chciałbym się z Wami podzielić.

Wzorzec Singleton jest jednym z najbardziej znanych wzorców programowania obiektowego. Polega on na utworzeniu satycznej metody, która ma za zadanie zwrócić za każdym razem dokładnie ten sam egzemplarz klasy. Stąd bardzo popularną nazwą tej metody jest getInstance() albo po prostu instance(). Czasami do takiego singletona dorzuca się prywatne konstruktory tak, aby osoba z niego korzystająca nie mogła utworzyć instancji tej klasy w inny sposób niż za pomocą metody do tego przeznaczonej.

Łatwo można skojarzyć, że Singleton daje nam dostęp do instancji w sposób globalny - nie ma żadnych ograniczeń związanych z wywoływaniem statycznej metody w którymkolwiek miejscu w kodzie. Jest to dosyć wygodne, jednakże - musimy liczyć się z pewnymi konsekwencjami, jeżeli zdecydujemy się na stosowanie tego wzorca.

Wyobraźmy sobie taką sytuację:

#include <exception>

class MagicCalculationOperation {

public:

    int calculate(int lhs, int rhs) {
        int magicResult = MagicCalculationOperationDriver::getInstance()->calculate(lhs, rhs);
        if (magicResult < 0) {
            throw std::exception("Invalid magic result");
        }
    }

};

Wszystko tutaj wydaje się w porządku, do momentu, w którym będziemy chcieli napisać test. Podczas pisania testu zakładamy, że nie wiemy, w jaki sposób działa metoda MagicCalculationOperationDriver::calculate(). Zależy nam jedynie na sprawdzeniu, czy jeżeli zostanie zwrócona wartość zerowa, to zostanie wyrzucony wyjątek.

Gdybyśmy napisali test na początku (przed powstaniem kodu produkcyjnego), to na pewno wstrzyknęlibyśmy nasz obiekt klasy MagicCalculationOperationDriver do konstruktora, a podczas testu do konstuktora klasy MagicCalculationOperation przekazalibyśmy jakiś obiekt maskujący (mock), którego klasa calculate() zwróciłaby wartość zerową. Niestety, powyższy kod nie daje nam takiej możliwości. Jedyne, co moglibyśmy zrobić, to próba jakiegokolwiek nadpisania statycznej metody tak, aby ta zwracała nam egzemplarz innej klasy. Z doświadczenia jednak wiem, że jest to bardzo karkołomne (metoda statyczna nie może być wirtualna).

Jak dobrze rozwiązać problem jednej instancji

Jeżeli chcielibyśmy, aby nasz kod był testowalny oraz jednocześnie korzystał z jedno-instancyjnych klas, dobrym wyjściem będzie skorzystanie z wzorca wstrzykiwania zależności w połączeniu z systemem rozwiązywania zależności w kodzie (nie nie, nie chodzi o Conana ;)).

Wzorzec wstrzykiwania zależności (ang. Dependency Injection) polega na wstrzykiwaniu do konstruktora obiektów wszystkich klas, z których chcielibyśmy skorzystać w klasie zależnej. Jeżeli nasza zależność będzie obiektem klasy wirtualnej, to zyskujemy możliwość wstrzyknięcia klasy rozszerzającej ją. Jeżeli mamy ten punkt, to możemy w bardzo łatwy sposób testować nasze klasy, wstrzykując obiekty klas-marionetek (mock objects), które pozwolą nam przetestować nasz kod w dowolny sposób.

Dla wstrzykiwania zależności nie ma znaczenia, czy zamierzamy wstrzyknąć nowo utworzony obiekt, czy obiekt jednej instancji. Możemy zatem użyć zewnętrznego narzędzia, które będzie pilnowało zasady jednej instancji za nas. Przykładem takiego narzędzia jest Boost Di, który serdecznie Wam polecam. Biblioteka jest w fazie eksperymentalnej, jednak myślę, że można sobie tym nie zaprzątać głowy - biblioteka Boost znana jest z oprogramowania świetnej jakości. Jeżeli jednak Boost Di Wam nie odpowiada, to możecie znaleźć zamiennik. W końcu temat rozwiązywania zależności w kodzie jest bardzo powszechny.

Wracając do przypadku z GoogleTest…

Powróćmy jeszcze na chwilę do tematu głównego dzisiejszego wpisu. O ile wzorzec Singleton ma swoją stanowczą wadę, to wykorzystanie go w tym przypadku dało nam coś, co raczej ciężko by było uzyskać w inny sposób. Myślę, że ten zabieg był całkowicie świadomy i jest akceptowany przez wiele osób, ze względu na automatyczne rejestrowanie testów, które potrafi być uciążliwe. Pomimo kontrowersji związanych z Singletonem, myślę że tutaj mamy do czynienia z bardziej pragmatycznym podejściem niż pójściem na skróty.

Kolejność uruchomienia a GoogleTest

Ostatnim pytaniem, nad którym się dziś pochylimy jest: “Skąd mamy pewność, że najpierw wszystkie testy zostaną zarejestrowane, a na samym końcu uruchomione?”. Odpowiedź jest prosta. Standard C++ określa, że przed uruchomieniem funkcji main() muszą zostać zainicjalizowane wszystkie zmienne globalne oraz statyczne pola klas. Ponieważ rejestracja testu jest schowana wewnątrz inicjalizacji obiektu klasy ::testing::TestInfo, odbywa się ona w dokładnie tym samym czasie co inicjalizacja. Dopiero na samym końcu zostaje uruchomiona funkcja main().

Myślę, że poniższy kod zobrazuje Wam cały proces:

Plik a.cpp:

#include <iostream>

class A {

public:

    A(int a) {
        std::cout << "A " << a << std::endl;
    }
};

A createA(int a) {
    return A(a);
}

A a = createA(10);

Plik main.cpp:

#include <iostream>

int main() {
    std::cout << "Main" << std::endl;
    return 0;
}

Niezależnie od kolejności, w jakiej skompilowane zostaną obydwa pliki, na wyjściu naszego programu pojawi się najpierw A 10, a potem dopiero Main.

Jeżeli chcecie coś więcej poczytać na temat inicjalizacji zmiennych statycznych, to zachęcam Was do przeczytania wpisu na blogu Bartłomieja Filipka: What happens to your static variables at the start of the program?.



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