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

Testy jednostkowe z GoogleTest


2019-11-12, 02:00

Profesjonalny programista to nie programista, który nie popełnia błędów. Profesjonalny programista to programista, który popełnia ten sam błąd maksymalnie jeden raz. Tą krótką mądrością zapraszam Was na wpis poświęcony świetnemu i jakże prostemu narzędziu, którym jest biblioteka GoogleTest.

Świat profesjonalistów

Tworząc oprogramowanie, powinniśmy to robić z pełnym profesjonalizmem. Każdy bowiem oczekuje, że produkt przez niego zakupiony jest idealny. Niezależnie od tego, czym zajmuje się tworzona przez nas aplikacja, powinniśmy dawać gwarancję na jej poprawne działanie. Aby móc dać taką gwarancję, najpierw trzeba poznać wymagania, które zostaną następnie zaprogramowane. Dopiero po poznaniu tych wymagań jesteśmy w stanie sprawdzić, czy nasza aplikacja im sprosta. Zarówno do określenia wymagań, jak i do sprawdzenia spełniania przez aplikację wymagań służą nam testy automatyczne, o których właśnie jest dzisiejszy wpis :)

Testowanie na wielu płaszczyznach

Testować naszą aplikację możemy na wiele różnych sposobów. W zależności od naszych potrzeb, możemy potrzebować sprawdzić, czy funkcja oblicza wynik w poprawny sposób, plik został zapisany poprawnie, bądź też czy po kliknięciu w przycisk wykonana zostanie oczekiwana operacja. Niezależnie od naszej intencji, można znaleźć pewne wspólne cechy, które pomogą nam pogrupować nasze testy tak, aby łatwiej nam było się odnaleźć.

Testy automatyczne możemy podzielić na¹:

  • Testy jednostkowe, podczas których wybieramy sobie jednostkę (funkcję, metodę lub klasę) i testujemy jej zachowania w odseparowaniu od całej aplikacji. Testy jednostkowe muszą być również odseparowane od świata zewnętrznego.

  • Testy integracyjne, podczas których testujemy interakcje pomiędzy różnymi funkcjami/klasami. Podobnie jak testy jednostkowe, działamy w pełnym odseparowaniu od świata zewnętrznego.

  • Testy end-to-end, które sprawdzają poprawność naszej aplikacji jako całości. W przeciwieństwie do testów jednostkowych oraz integracyjnych, tutaj działamy już w połączeniu ze światem zewnętrznym.

Teoria testowania jednostkowego

Testy jednostkowe uchodzą za najszybsze z wspomnianych przeze mnie typów testów. Testując kod jednostkowo, sprawdzamy jedynie mały fragment kodu (w przeciwieństwie do testów integracyjnych, gdzie możemy korzystać np. z mechanizmów automatycznego wstrzykiwania zależności, dyspozytora komunikatów itp).

Testując jednostkowo, próbujemy wejść w interakcję z wybraną przez nas jednostką, kontrolując jej zachowania. Do tych zachowań zaliczyć możemy:

  • zgodność parametrów przekazywanych do jednostki
  • zgodność zwracanych przez jednostkę wartości
  • wyrzucane przez jednostkę wyjątki

Zarówno funkcja, metoda (która jest niejako funkcją, lecz w konkretnym kontekście) oraz klasa (kontekst zawierający zestaw pól oraz metod) posiadają ten zestaw zachowań, dlatego możemy spośród nich wybrać ten rodzaj jednostki, który będzie najbardziej odpowiadał naszym potrzebom.

Mechanika testowania jednostkowego jest bardzo prosta i raczej niezależna od technologii, w obrębie której działamy. Jednak dla lepszego zrozumienia omówimy ją działając bezpośrednio w oparciu o bibliotekę GoogleTest.

Przechodzimy do GoogleTest

Źródła biblioteki GTest znajdziecie pod tym linkiem, jednak my do konfiguracji projektu skorzystamy z menadżera zależności Conan w połączeniu z CMake. Jeżeli nie macie doświadczenia z Conanem, to zapraszam do ostatniego wpisu, w którym omawiałem związane z nim podstawy. Jeżeli nie wiecie zaś czym jest CMake, to zapraszam pod link.

Konfiguracja z wykorzystaniem Conana i CMake

Na sam początek w głównym katalogu projektu tworzymy plik conanfile.txt z zawartością:

[requires]
gtest/1.8.0@bincrafters/stable
[generators]
cmake

Następnie uruchamiamy pare komend w terminalu:

mkdir build
cd build
conan install ..

Czyli: tworzymy katalog build, następnie wchodzimy do niego i z jego poziomu instalujemy bibliotekę GoogleTest. Po wykonaniu ostatniej komendy do katalogu build zostanie dodanych kilka plików, w tym conanbuildinfo.cmake, z którego zaraz skorzystamy.

Po instalacji GoogleTest tworzymy plik CMakeLists.txt z konfiguracją budowania projektu:

cmake_minimum_required(VERSION 3.10)
project(MyApplication)

include(${CMAKE_BINARY_DIR}/conanbuildinfo.cmake)
conan_basic_setup()

file(GLOB_RECURSE MY_APP_SRC ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp)
add_library(my_app_src ${MY_APP_SRC})
target_include_directories(my_app_src PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/src)

add_executable(my_app ${CMAKE_CURRENT_SOURCE_DIR}/main.cpp)
target_link_libraries(my_app my_app_src)

file(GLOB_RECURSE MY_APP_TEST_SRC ${CMAKE_CURRENT_SOURCE_DIR}/tests/*.cpp)
add_executable(tests ${MY_APP_TEST_SRC})
target_link_libraries(tests my_app_src)
target_link_libraries(tests ${CONAN_LIBS})

Trochę dużo się tutaj dzieje, więc już lecimy z wyjaśnieniem.

Pierwsze dwie linijki określają wersję CMake oraz nazwę aplikacji. Skonfigurujcie tą część według uznania. Dalej inicjalizujemy biblioteki zainstalowane za pośrednictwem Conana. Następnie plik dzielimy na trzy części:

  1. Zbieramy wszystkie pliki z umownego katalogu src, w którym są zawarte wszystkie pliki źródłowe niezawierające entrypointu (czyli bez definicji funkcji main()). i generujemy z nich bibliotekę statyczną. Do tej biblioteki dołączamy również wszystkie nagłówki znajdujące się wewnątrz katalogu src.

  2. Tworzymy plik wykonywalny na podstawie pliku main.cpp znajdującego się w głównym katalogu projektu. Do tego pliku linkujemy skompilowaną już bibliotekę statyczną z logiką naszej aplikacji.

  3. Tworzymy plik wykonywalny zawierający testy. Wszystkie pliki testów (wraz z plikiem definiującym funkcję main()) będą znajdowały się w katalogu tests/. Do targetu z testami linkujemy (podobnie jak w punkcie drugim) bibliotekę statyczną zawierającą kod logiki naszej aplikacji. Na samym końcu do testów linkujemy bibliotekę GoogleTest, której nazwa znajduje się w zmiennej CONAN_LIBS.

Powyższe rozwiązanie powoduje, że logika biznesowa naszej aplikacji będzie skompilowana tylko raz. Jest to szczególnie dobre rozwiązanie wśród dużych projektów, które wykorzystują środowisko Continuous Integration do automatycznej kompilacji i uruchamiania testów.

Plikiem main.cpp każdy sobie da radę, przejdźmy zatem do katalogu tests, a dokładniej do pliku tests/main.cpp:

#include <gtest/gtest.h>

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

To, na co warto zwrócić uwagę, to linijki 4 i 5. Plik uruchomieniowy testów możemy skonfigurować dowolnie, jednak musimy pamiętać, aby:

  1. Funkcja main() zwróciła wynik makra RUN_ALL_TESTS().
  2. Przed wywołaniem makra RUN_ALL_TESTS() musi zostać wywołana metoda ::testing::InitGoogleTest(...).

Wspomnieć warto, że cała biblioteka działa w oparciu o wzorzec Singletone: podczas definiowania testu do głównej instancji wykonawczej zostaje zarejestrowana informacja o nim samym. Następnie instancja ta iteruje po zarejestrowanych testach, wykonując ich logikę.

Cały proces konfiguracji jest już za nami. Możemy przejść do pisania testów.

Piszemy pierwsze testy

Przypatrzmy się poniższemu listingowi:

#include <gtest/gtest.h>
#include "Infrastructure/MD5.hpp"

TEST(MD5_Algorithm, test_hash) {
    MD5 hash("content");
    ASSERT_EQ("9a0364b9e99bb480dd25e1f0284c8555", hash.getHash());
}

TEST(MD5_Algorithm, test_two_hashes_equal) {
    MD5 hash1("content");
    MD5 hash2("content");
    ASSERT_EQ(hash2.getHash(), hash1.getHash());
    ASSERT_TRUE(hash1.isEqual(hash2));
    ASSERT_TRUE(hash2.isEqual(hash1));
}

Na początku załączamy do pliku główny nagłówek biblioteki GoogleTest. Następnie dołączamy nagłówek z klasą, którą mamy zamiar przetestować. W moim przypadku jest to klasa generująca hash MD5.

Jak się zapewne spodziewacie, każdy test rozpoczynamy korzystając z makra TEST(). Pierwszy parametr nosi nazwę Test Case (stosowana wymiennie z nazwą Test Suite) i służy do grupowania podobnych do siebie testów. Drugim parametrem jest nazwa testu. GoogleTest w celu identyfikacji pojedynczego testu skleja ze sobą obydwie te wartości, dlatego każda para Test Case i Test Name powinna być unikalna. Pomiędzy klamrami znajduje się kod testujący, który w nomenklaturze tej biblioteki nosi nazwę Test Body.

Tworząc pojedynczy test (tudzież komponując Test Body), najczęściej będziemy to robić w trzech krokach:

  • Arrange - krok początkowy, podczas którego inicjalizujemy wszystkie potrzebne do testu elementy (zmienne, obiekty, struktury)
  • Act - krok drugi, w którym wywołujemy testowaną jednostkę w oparciu o zainicjalizowane wcześniej dane
  • Assert - ostatni krok, w którym dokonujemy sprawdzania wyników osiągniętych w poprzednim kroku

Kroki te zawsze występują po sobie w kolejności wymienionej przeze mnie. W niektórych przypadkach (tak jak to jest u nas obecnie) sąsiednie kroki scalają się w jeden. W naszym przypadku krok Arrange połączony zostaje z krokiem Act, ponieważ testujemy działanie konstruktora. Niemniej, pamiętać trzeba, że wszystkie te kroki następują po sobie, czasami w sposób niejawny.

Makra z rodziny ASSERT_ i EXPECT_

O ile pierwsze dwa kroki możemy wykonać bez korzystania z biblioteki GoogleTest, to już w kroku trzecim będziemy potrzebowali skorzystać z makr przez nią zdefiniowanych.

Wyróżniamy dwie rodziny makr dokonujących sprawdzań wewnątrz testów: ASSERT_ oraz EXPECT_. Obydwie z nich definiują taki sam zestaw sprawdzań, różniących się tylko jednym zachowaniem:

  • makra z rodziny ASSERT_ po negatywnym wyniku sprawdzania warunku kończy wykonywanie procesu testowania, zgłaszając natychmiastowo błąd
  • makra z rodziny EXPECT_ po negatywnym wyniku sprawdzania kontynuują proces testowania

Polecam domyślnie korzystać z makr rozpoczynających się od prefiksu EXPECT_, a wszędzie tam, gdzie dalsze testowanie nie ma już sensu, należy korzystać z makr z przedrostkiem ASSERT_.

Najczęściej wykorzystywane makra

Dla wygody czytania będę korzystał z przedrostka EXPECT_. Wszystkie wymienione przeze mnie makra występują zarówno z prefiksem EXPECT_ jak i ASSERT_.

Pierwszą grupą makr testujących będą te porównujące wartości typu bool:

  • EXPECT_TRUE - sprawdza, czy wartość podana w parametrze jest prawdą
  • EXPECT_FALSE - sprawdza, czy wartość podana w parametrze jest fałszem

Zaraz po nich należy zapoznać się z makrami wykorzystującymi do porównania proste operatory porównań:

  • EXPECT_EQ - porównuje dwie wartości korzystając z operatora ==
  • EXPECT_NE - porównuje dwie wartości korzystając z operatora !=
  • EXPECT_LT - porównuje dwie wartości korzystając z operatora <
  • EXPECT_LE - porównuje dwie wartości korzystając z operatora <=
  • EXPECT_GT - porównuje dwie wartości korzystając z operatora >
  • EXPECT_GE - porównuje dwie wartości korzystając z operatora >=

Dodać należy, że powyższe makra nie służą do porównywania wyłącznie typów prostych. Porównywać możemy również typy złożone (struktury, klasy) definiujące konkretny operator, jak na poniższym przykładzie:

#include <gtest/gtest.h>

class A {

public:

    A(int value): value(value) { }

    bool operator==(const A& lhs) const {
        return lhs.value == value;
    }

private:

    int value;
};

TEST(OperatorTest, test_equality_operator) {
    A a(10);
    A b(10);
    EXPECT_EQ(a, b);
}

Do porównywania liczb zmiennoprzecinkowych wykorzystujemy zestaw makr:

  • EXPECT_FLOAT_EQ - porównuje dwie wartości zmiennoprzecinkowe typu float
  • EXPECT_DOUBLE_EQ - porównuje dwie wartości zmiennoprzecinkowe typu double

Algorytm porównywania obydwu typów liczb zmiennoprzecinkowych jest identyczny i różni się wyłącznie precyzją typu:

AssertionResult CmpHelperFloatingPointEQ(
  const char* lhs_expression,
  const char* rhs_expression,
  RawType lhs_value,
  RawType rhs_value
) {
  const FloatingPoint<RawType> lhs(lhs_value), rhs(rhs_value);

  if (lhs.AlmostEquals(rhs)) {
    return AssertionSuccess();
  }

  // tutaj nieco dłuższy kod obsługujący niepowodzenie
}

static const size_t kMaxUlps = 4;

bool AlmostEquals(const FloatingPoint& rhs) const {
  // The IEEE standard says that any comparison operation involving
  // a NAN must return false.
  if (is_nan() || rhs.is_nan()) return false;

  return DistanceBetweenSignAndMagnitudeNumbers(u_.bits_, rhs.u_.bits_)
      <= kMaxUlps;
}

static Bits DistanceBetweenSignAndMagnitudeNumbers(
  const Bits &sam1, const Bits &sam2
) {
  const Bits biased1 = SignAndMagnitudeToBiased(sam1);
  const Bits biased2 = SignAndMagnitudeToBiased(sam2);
  return (biased1 >= biased2) ? (biased1 - biased2) : (biased2 - biased1);
}

Więcej o standardzie IEEE754 możecie przeczytać w moich poprzednich wpisach, o tutaj i tutaj.

W następnej kolejności warto wiedzieć o makrach porównujących ciągi znakowe:

  • EXPECT_STREQ - porównuje dwa ciągi znakowe za pomocą funkcji strcmp()
  • EXPECT_STRNE - wykonuje zaprzeczenie tej samej funkcji, którą wykorzystuje EXPECT_STREQ
  • EXPECT_STRCASEEQ - porównuje dwa ciągi znakowe bez rozróżniania wielkości liter przy pomocy funkcji strcasecmp()
  • EXPECT_STRCASENE - wykonuje zaprzeczenie tej samej funkcji, którą wykorzystuje EXPECT_STRCASEEQ

Funkcje wykorzystywane przy porównywaniu ciągów znakowych:

bool String::CStringEquals(const char * lhs, const char * rhs) {
  if ( lhs == NULL ) return rhs == NULL;

  if ( rhs == NULL ) return false;

  return strcmp(lhs, rhs) == 0;
}

bool String::CaseInsensitiveCStringEquals(const char * lhs, const char * rhs) {
  if (lhs == NULL)
    return rhs == NULL;
  if (rhs == NULL)
    return false;
  return posix::StrCaseCmp(lhs, rhs) == 0;
}

namespace posix {
    inline int StrCaseCmp(const char* s1, const char* s2) {
      return strcasecmp(s1, s2);
    }
}

Ostatnią grupą makr wartych uwagi są te, które sprawdzają czy i jakiego typu wyjątek został wyrzucony:

  • EXPECT_ANY_THROW - Oczekujemy, że wywoływana w parametrze metoda wyrzuci jakikolwiek wyjątek.
  • EXPECT_THROW - Oczekujemy, że wywoływana metoda wyrzuci wyjątek określonego przez nas typu. W pierwszym parametrze robimy wywołanie funkcji/metody, w drugiej przekazujemy typ oczekiwanego przez nas wyjątku
  • EXPECT_NO_THROW - Oczekujemy, że wywoływana w parametrze metoda nie wyrzuci żadnego wyjątku.

Fixturki, czyli lepsza integracja między testami

Niejednokrotnie zachodzi potrzeba zacieśnienia integracji pomiędzy testami zawartymi wewnątrz Test Case. Taka potrzeba może pojawić się np. wtedy, kiedy podczas każdego testu tworzymy testowany obiekt w dokładnie ten sam sposób, lub część logiki pomiędzy testami powtarza się. Czasami jest potrzeba wykonać pewien kawałek kodu przed każdym testem, albo chociaż przed uruchomieniem pierwszego testu wewnątrz Test Case. Do takich przypadków przydadzą się metody specjalne, uruchamiane w stałych punktach procesu testowania :)

Wyobraźmy sobie, że nasze testy mogą posiadać pamięć, która będzie dla nich wspólna. Tak tak, ta przestrzeń to właśnie nasz Test Case. Niestety, w pierwszym podanym przeze mnie przykładzie obydwa testy są odizolowane od siebie pomimo, że korzystają z tego samego Test Case. Aby zintegrować dwa testy wewnątrz jednego Test Case, należy utworzyć klasę dla naszego Case, a następnie skorzystać z makra TEST_F przy każdym teście. Na przykład w taki sposób:

#include <gtest/gtest.h>
#include "Infrastructure/MD5.hpp"

class MD5_Algorithm_FixtureExample : public ::testing::Test {

protected:

    MD5 createHash(std::string value) {
        return MD5(value);
    }

};

TEST_F(MD5_Algorithm_FixtureExample, test_hash) {
    auto hash = createHash("content");
    ASSERT_EQ("9a0364b9e99bb480dd25e1f0284c8555", hash.getHash());
}

TEST_F(MD5_Algorithm_FixtureExample, test_two_hashes_equal) {
    auto hash1 = createHash("content");
    auto hash2 = createHash("content");
    ASSERT_EQ(hash2.getHash(), hash1.getHash());
    ASSERT_TRUE(hash1.isEqual(hash2));
    ASSERT_TRUE(hash2.isEqual(hash1));
}

Rozwiązanie to, może w tym przykładzie nieco naciągane, jest pomocne szczególnie wtedy, kiedy w jednym Test Case mamy wiele testów, a część wspólna logiki uległa zmianie. Zamiast poprawiać np. 20 testów, zmieniamy kod tylko w jednym miejscu.

W jaki sposób to działa? Rozwiązanie to jest bardzo proste: każdy test dziedziczy po naszej klasie MD5_Algorithm_FixtureExample, implementując metodę TestBody:

virtual void TestBody() = 0;

Zarówno nasz Test Case jak i każdy test ma zestaw metod, które uruchamiane są automatycznie, a w których możemy zawrzeć wspólny dla testów kod. Poniższy listing kodu przedstawia wszystkie te metody wraz z opisami:

class MD5_Algorithm_FixtureExample2 : public ::testing::Test {

protected:

    static void SetUpTestSuite() {
        // Kod uruchamiany przed pierwszym testem wewnątrz `Test Case`
    }

    MD5_Algorithm_FixtureExample2() {
        // Konstruktor (jak można się spodziewać) jest uruchamiany przy tworzeniu obiektu testu
    }

    void SetUp() override {
        // Kod uruchamiany po konstruktorze, przed uruchomieniem testu
    }

    void TearDown() override {
        // Kod uruchamiany po wykonaniu kodu testu, przed wykonaniem destruktora
    }

    ~MD5_Algorithm_FixtureExample2() {
        // Destruktor uruchamiany podczas niszczenia obiektu testu
    }

    static void TearDownTestSuite() {
        // Kod uruchamiany po ostatnim teście wewnątrz `Test Case`
    }
};

O ile metody SetUp() oraz TearDown() uruchamiane są na poziomie pojedynczego testu, to już metody SetUpTestSuite() oraz TearDownTestSuite() uruchamiane są na samym początku naszego Test Case, dzięki czemu możemy zainicjalizować oraz zdeinicjalizować wszystkie współdzielone pomiędzy testami obiekty. Pamiętajmy jednak, że jeżeli potrzebujemy przechowywać współdzielone dane w klasie naszego Test Case, to muszą być one statyczne! Jest to spowodowane mechanizmem, który tworzy test tuż przed jego uruchomieniem.

Kiedy korzystać z konstruktora/destruktora, a kiedy z SetUp/TearDown?

Na pewno zastanawiacie się, po co nam są potrzebne metody SetUp() oraz TearDown(), skoro mamy do dyspozycji konstruktor i destruktor? Pomimo, że są to metody wywoływane w prawie tym samym czasie, są drobne różnice, o których należy pamiętać korzystając z obydwu zestawów:

  • Kiedy korzystamy z konstruktora/destruktora, zawsze wiemy, w jakiej kolejności zostaną one wywołane w łańcuchu dziedziczenia - te zachowania są stałe. Wykonaniem metod SetUp() oraz TearDown() możemy sterować, dlatego nie jest to takie oczywiste.

  • Zarówno w konstruktorze ani destruktorze nie możemy korzystać z makr z rodziny ASSERT_. Korzystanie z tego typu makr może zaburzyć proces inicjalizacji/deinicjalizacji testu (np. przez błędy typu fatal failure). Jeżeli nie jesteśmy pewni, czy nasz kod może zaburzyć proces inicjalizacji/deinicjalizacji, skorzystajmy z metod SetUp() oraz TearDown().

  • Jeżeli podczas procesu deinicjalizacji zostawiamy możliwość wyrzucenia wyjątku, to niewątpliwie powinniśmy skorzystać z metody TearDown(). Wyrzucanie wyjątków w destruktorze skutkuje Undefined Behavior, które czasami potrafi zakończyć działanie programu, niezależnie od tego, czy wyjątek miał zostać złapany, czy nie.

  • Wewnątrz konstruktora/destruktora nie ma możliwości wywołania metody wirtualnej. Wywołanie takiej metody nie będzie skutkować błędem kompilatora - wykonana zostanie metoda zbindowana statycznie. Jeżeli planujemy uruchamiać metody wirtualne, powinniśmy wybrać metody SetUp() oraz TearDown().

Uruchamianie wybranego zestawu testów

Kiedy już napisaliśmy trochę testów, pora dowiedzieć się, w jaki sposób możemy uruchomić je i sprawdzić, czy wszystkie assercje zostają spełnione. Aby uruchomić nasze testy, przechodzimy do katalogu build/bin a następnie uruchamiamy plik wykonywalny tests:

cd build/bin
./tests

Na standardowym wyjściu powinno zostać wypisane coś na wzór:

[==========] Running 4 tests from 2 test cases.
[----------] Global test environment set-up.
[----------] 2 tests from MD5_Algorithm
[ RUN      ] MD5_Algorithm.test_hash
[       OK ] MD5_Algorithm.test_hash (0 ms)
[ RUN      ] MD5_Algorithm.test_two_hashes_equal
[       OK ] MD5_Algorithm.test_two_hashes_equal (0 ms)
[----------] 2 tests from MD5_Algorithm (0 ms total)

[----------] 2 tests from MD5_Algorithm_FixtureExample
[ RUN      ] MD5_Algorithm_FixtureExample.test_hash
[       OK ] MD5_Algorithm_FixtureExample.test_hash (0 ms)
[ RUN      ] MD5_Algorithm_FixtureExample.test_two_hashes_equal
[       OK ] MD5_Algorithm_FixtureExample.test_two_hashes_equal (0 ms)
[----------] 2 tests from MD5_Algorithm_FixtureExample (0 ms total)

[----------] Global test environment tear-down
[==========] 4 tests from 2 test cases ran. (0 ms total)
[  PASSED  ] 4 tests.

Im więcej testów, tym większy output zostanie wygenerowany, a na sprawdzenie, czy aktualnie poprawiane przez nas testy działają będzie trzeba czekać. Na szczęście dostępna jest opcja na filtrowania, która może nam oszczędzić nieco zasobów.

Aby przefiltrować testy, by wykonać tylko te spod konkretnego Test Case, należy przekazać opcję --gtest_filter z odpowiednią wartością:

bin/tests --gtest_filter=MD5_Algorithm.*

Powyższa komenda uruchomi tylko dwa spośród czterech zdefiniowanych przez nas testów:

Note: Google Test filter = MD5_Algorithm.*
[==========] Running 2 tests from 1 test case.
[----------] Global test environment set-up.
[----------] 2 tests from MD5_Algorithm
[ RUN      ] MD5_Algorithm.test_hash
[       OK ] MD5_Algorithm.test_hash (0 ms)
[ RUN      ] MD5_Algorithm.test_two_hashes_equal
[       OK ] MD5_Algorithm.test_two_hashes_equal (0 ms)
[----------] 2 tests from MD5_Algorithm (0 ms total)

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

Jeżeli chcemy uruchomić konkretny test, to robimy to w taki sposób:

bin/tests --gtest_filter=MD5_Algorithm.test_hash

Po wpisaniu tej komendy na wyjściu zostanie wypisane:

Note: Google Test filter = MD5_Algorithm.test_hash
[==========] Running 1 test from 1 test case.
[----------] Global test environment set-up.
[----------] 1 test from MD5_Algorithm
[ RUN      ] MD5_Algorithm.test_hash
[       OK ] MD5_Algorithm.test_hash (0 ms)
[----------] 1 test from MD5_Algorithm (0 ms total)

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

Jeżeli chcemy uruchomić kilka testów, to łączymy je za pomocą znaku ::

 bin/tests --gtest_filter=MD5_Algorithm.test_hash:MD5_Algorithm_FixtureExample.test_hash

Co da nam na wyjściu:

Note: Google Test filter = MD5_Algorithm.test_hash:MD5_Algorithm_FixtureExample.test_hash
[==========] Running 2 tests from 2 test cases.
[----------] Global test environment set-up.
[----------] 1 test from MD5_Algorithm
[ RUN      ] MD5_Algorithm.test_hash
[       OK ] MD5_Algorithm.test_hash (0 ms)
[----------] 1 test from MD5_Algorithm (0 ms total)

[----------] 1 test from MD5_Algorithm_FixtureExample
[ RUN      ] MD5_Algorithm_FixtureExample.test_hash
[       OK ] MD5_Algorithm_FixtureExample.test_hash (0 ms)
[----------] 1 test from MD5_Algorithm_FixtureExample (0 ms total)

[----------] Global test environment tear-down
[==========] 2 tests from 2 test cases ran. (0 ms total)
[  PASSED  ] 2 tests.

Jeżeli widzicie tutaj gdzieś wzorzec, to na pewno uda Wam się wykombinować polecenie uruchamiające np. dwa różne Test Case :)

Kod źródłowy

Jeżeli chcecie pobawić się kodem z przykładów zawartych w tym wpisie, możecie ściągnąć źródła z repozytorium na GitHubie.

Podsumowanie

Gratuluję, jeżeli dotrwaliście do końca tego długiego nieco wpisu :) Poznaliśmy dzisiaj krótką teorię na temat testów jednostkowych oraz dowiedzieliśmy się, czym jest jednostka. Po części teoretycznej przeszliśmy do części praktycznej, gdzie skonfigurowaliśmy bibliotekę GoogleTest za pomocą menadżera zależności Conan oraz CMake. Poznaliśmy również mechainkę działania bibliotekii GoogleTest: testy, assercje oraz fixturki.


¹ Oczywiście, istnieje więcej rodzajów testów. Wymieniłem jedynie te, z którymi najczęściej możemy się spotkać pracując jako programista tworzący funkcjonalności.



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