Google C++ Testing Framework

Podstawowe koncepcje

W frameworkach do TDD, takich jak Google Test pracę zaczynamy od pisania asercji, czyli wyrażeń, które sprawdzają, czy warunek jest spełniony.

Rezultatem asercji może być:

  • sukces
  • błąd niekrytyczny
  • błąd krytyczny

Podstawowe asercje

Krytyczne Niekrytyczne Sprawdza
ASSERT_TRUE(warunek) EXPECT_TRUE(warunek) Czy warunek jest prawdziwy
ASSERT_FALSE(warunek) EXPECT_FALSE(warunek) Czy warunek jest fałszywy

ASSERT_* powoduje, że zostaje przerwane wykonywanie bieżącego testu, a EXPECT_* kontynuuje jego wykonywanie.

Porównywanie

Krytyczne Niekrytyczne Sprawdza
ASSERT_EQ(expexcted, actual); EXPECT_EQ(expected, actual); expected == actual
ASSERT_NE(val1, val2); EXPECT_NE(val1, val2); val1 != val2
ASSERT_LT(val1, val2); EXPECT_LT(val1, val2); val1 < val2
ASSERT_LE(val1, val2); EXPECT_LE(val1, val2); val1 <= val2
ASSERT_GT(val1, val2); EXPECT_GT(val1, val2); val1 > val2
ASSERT_GE(val1, val2); EXPECT_GE(val1, val2); val1 >= val2

Argumenty przekazane do asercji muszą umożliwiać odpowiednie porównanie poprzez dostarczenie odpowiadających operatorów.

Argumenty mogą również dostarczyć operator << do komunikacji przy pomocy std::ostream. Taki operator zostanie wykorzystany przez Google Test do prezentacji argumentu.

Proste testy

Aby utworzyć test, należy użyć makra TEST() w celu zdefiniowania nazwy testu. TEST() jest zwykłą funkcją C++ niezwracającą żadnej wartości. Wewnątrz ciała funkcji można umieszczać dowolne wyrażenia języka.

TEST(PrimeTest, SomeNumbersArePrimes)
{
   ASSERT_TRUE(is_prime(2));
}

TEST(ArrayEquality, ArrayEqualitySimple)
{
    int arr1[5] = {1, 2, 3, 4, 5};
    int arr2[5] = {1, 2, 3, 2, 5};
    for (int i = 0; i < 5; ++i)
    {
        EXPECT_EQ(arr1[i], arr2[i]) << "for index: " << i;
    }
}

Za pomocą operatora << można przekazać do asercji dodatkowe informacje, które zostaną wypisane obok ewentualnego błędu.

Aby uruchomić test, można skorzystać z gotowej funkcji main znajdującej się w gtest_main lub napisać własną funkcję.

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

Rezultatem będzie raport o przeprowadzonych testach.

[==========] Running 2 tests from 2 test cases.
[----------] Global test environment set-up.
[----------] 1 test from PrimeTest
[ RUN      ] PrimeTest.SomeNumbersArePrimes
[       OK ] PrimeTest.SomeNumbersArePrimes (1 ms)
[----------] 1 test from PrimeTest (7 ms total)

[----------] 1 test from ArrayEquality
[ RUN      ] ArrayEquality.ArrayEqualitySimple
..\simple.cpp:27: Failure
Value of: arr2[i]
  Actual: 2
Expected: arr1[i]
Which is: 4
for index: 3
[  FAILED  ] ArrayEquality.ArrayEqualitySimple (9 ms)
[----------] 1 test from ArrayEquality (15 ms total)

[----------] Global test environment tear-down
[==========] 2 tests from 2 test cases ran. (51 ms total)
[  PASSED  ] 1 test.
[  FAILED  ] 1 test, listed below:
[  FAILED  ] ArrayEquality.ArrayEqualitySimple

 1 FAILED TEST

Fikstury

Jeśli tworzymy testy operujące na tym samym zestawie danych, można użyć klasy dziedziczącej po ::testing::Test, która będzie umożliwiała ponowne użycie kodu:

  • Wewnątrz klasy definiujemy wszystkie obiekty, których użycie jest planowane w testach * pola klasy powinny być zadeklarowane jako protected
  • Można również metod pomocniczych
  • Przygotowanie obiektu fikstury odbywa się albo w konstruktorze, albo przy pomocy specjalnej metody SetUp()
  • Jeśli zachodzi potrzeba zwalniana zasobów, można użyć destruktora lub metody TearDown(), która w przeciwieństwie do destruktora może wygenerować wyjątek

Ważne

Jeśli używamy fikstury, to należy użyć makra TEST_F(…) zamiast TEST(…)

class VectorTests : public ::testing::Test
{
protected:
    std::vector<int> vec;
public:
    VectorTests()
    {
        // you can set-up work for each test
        std::cout << "Inside fixture constructor" << std::endl;
        for (int i = 1; i <= 5; ++i)
        {
            vec.push_back(i);
        }
    }

    ~VectorTests() override
    {
        // you can clean-up work that does not THROW exceptions
    }

    void SetUp() override
    {
        // Code here will be called immediately after the constructor (right
        // before each test).
    }

    void TearDown() override
    {
        // Code here will be called immediately after each test (right
        // before the destructor).
    }
};

TEST_F(VectorTests, VectorEquality)
{
    int expected[5] = { 1, 2, 3, 2, 5 };

    for (int i = 0; i < 5; ++i)
    {
        EXPECT_EQ(vec[i], expected[i]) << "for index: " << i;
    }
}

Dzielenie zasobów pomiędzy testami

Ponieważ konstruktor SetUp() fikstury jest wywoływany przed rozpoczęciem każdego następnego testu, może mieć to duży wpływ na wydajność procesu testowania. Jeśli zasób jest używany tylko do odczytu to bezpieczne jest dzielenie go pomiędzy testami.

Przykład:

class SharedArrayTests : public ::testing::Test
{
protected:
    static std::vector<int> vec;

    static void SetUpTestCase()
    {
        std::cout << "Inside static fixture constructor" << std::endl;
        for (int i = 1; i <= 5; ++i)
        {
            vec.push_back(i);
        }
    }
};

std::vector<int> SharedArrayTests::vec;

TEST_F(SharedArrayTests, ArrayTestFirst)
{
    EXPECT_EQ(vec[0], 1);
}

Testowanie wyjątków

Jeśli nasz kod posługuje się wyjątkami, to ważne jest sprawdzenie czy testowany kod poprawnie je obsługuje. Zapewniają to poniższe asercje:

Krytyczne Niekrytyczne Sprawdza
ASSERT_THROW( expr, type ); EXPECT_THROW( expr, type ); Wyrażenie wygenerowało wyjątek określonego typu
ASSERT_ANY_THROW( expr ); EXPECT_ANY_THROW( expr ); Wyrażenie wygenerowało wyjątek dowolnego typu
ASSERT_NO_THROW( expr ); EXPECT_NO_THROW( expr ); Wyrażenie nie wygenerowało żadnego wyjątku

Przykładowo:

void simple_crash()
{
    throw std::runtime_error("ERROR");
}

TEST(ExceptionTests, SimpleCrashTrowsException)
{
    EXPECT_THROW(simple_crash(), std::runtime_error);
}

Porównywanie liczb zmiennoprzecinkowych

Porównywanie liczb zmiennoprzecinkowych bywa trudne, dlatego Google Test dostarcza specjalnych testów.

Krytyczne Niekrytyczne Sprawdza
ASSERT_FLOAT_EQ( a, b ) EXPECT_FLOAT_EQ( a, b ) Czy liczby są niemal równe
ASSERT_NEAR( a, b, margin ) EXPECT_NEAR( a, b, margin ) Czy liczby są równe z określonym marginesem

Niemal równe oznacza błąd mniejszy niż cztery ULP – Units in the Last Place, czyli ostatnich znaczących bitów.

TEST(FloatEquality, SimpleError)
{
    float a = 1.0;
    float b = 1.0 + 1e-7;
    EXPECT_FLOAT_EQ(a, b) << a << "=" << b;
}

TEST(FloatEquality, MarginError)
{
    float a = 1;
    float b = 1.1;
    EXPECT_NEAR(a, b, 0.2);
}

Death Tests

Są to testy sprawdzające poprawne zakończenie się pracy programu.

Krytyczne Niekrytyczne Sprawdza
ASSERT_DEATH(
statement, regex);
EXPECT_DEATH(
statement, regex`);
statement kończy pracę programu z podanym błędem
ASSERT_EXIT(
statement, predicate, regex);
EXPECT_EXIT(statement,
predicate, regex);
statement kończy pracę programu z podanym błędem,
a kod błędu spełnia predykat predicate

A predykaty to:

  • ::testing::ExitedWithCode(exit_code)
  • ::testing::KilledBySignal(signal_number)  // Not available on Windows
bool is_prime(long n)
{
    if (n > 0)
    {
        // some implementation
    }
    else
    {
        std::cerr << "Error: Negative or zero input\n";
        exit(-1);
    }
}

TEST(PrimeTest, PrimesForPositiveNumbers)
{
    ASSERT_EXIT(
        is_prime(-1), ::testing::ExitedWithCode(-1),
        "Error: Negative or zero input"
    );
}

Bezpośrednie wywoływanie sukcesu lub porażki testu

Można wymusić wygenerowanie „sukcesu” poprzez użycie makra SUCCEED();

Nie oznacza to że cały test się powiódł. Sukces testu występuje gdy każda jego asercja zakończyła się powodzeniem.

  • FAIL() generuje błąd krytyczny testu
  • ADD_FAILURE() generuje błąd niekrytyczny
switch(expression)
{
  case 1:
      break;
  case 2:
      break;
  default:
    FAIL() << "Nie powinno nas tu byc";
}

Testy parametryzowane

Framework GoogleTest umożliwia tworzenie testów, które są parametryzowane zestawami danych.

Aby przygotować parametryzowane testy dla funkcji:

int sum(int a, int b)
{
    return a + b;
}

Należy przygotować:

  • strukturę przechowującą zestaw parametrów testu:

    struct SumTestParams
    {
        int a, b, expected;
    
        SumTestParams(int a, int b, int expected)
            : a{a}, b{b}, expected{expected}
        {
        }
    };
    
  • fiksturę testów parametrycznych

    struct ParametrizedTest : public testing::TestWithParam<SumTestParams>
    {
    };
    

Kolejnym krokiem jest przygotowanie testu za pomocą makra TEST_P:

TEST_P(ParametrizedTest, AddingTwoNumbers)
{
    SumTestParams params = GetParam();
    ASSERT_EQ(sum(params.a, params.b), params.expected);
}

Funkcja GetParam() umożliwia zwrócenie obiektu agregującego parametry testów - instancji SumTestParams.

Ostatnim krokiem jest utworzenie tablicy parametrów i zainicjowanie nią zestawu testów:

SumTestParams params[] = { {1, 2, 3}, {5, 6, 11}, {665, 1, 666} };

INSTANTIATE_TEST_CASE_P(PackOfTests, ParametrizedTest, testing::ValuesIn(params));

Wynik wykonania testów parametrycznych:

[----------] 3 tests from PackOfTests/ParametrizedTest
[ RUN      ] PackOfTests/ParametrizedTest.AddingTwoNumbers/0
[       OK ] PackOfTests/ParametrizedTest.AddingTwoNumbers/0 (0 ms)
[ RUN      ] PackOfTests/ParametrizedTest.AddingTwoNumbers/1
[       OK ] PackOfTests/ParametrizedTest.AddingTwoNumbers/1 (0 ms)
[ RUN      ] PackOfTests/ParametrizedTest.AddingTwoNumbers/2
[       OK ] PackOfTests/ParametrizedTest.AddingTwoNumbers/2 (0 ms)
[----------] 3 tests from PackOfTests/ParametrizedTest (0 ms total)

Testy dla list typów

W przypadku testów pisanych dla klas generycznych możemy uniknąć duplikacji logiki testów stosując tzw. testy typizowane (typed tests). W takim przypadku zamiast makr TEST lub TEST_F należy zastosować makro TYPED_TEST.

Pierwszym krokiem jest zdefiniowanie fikstury jako szablonu klasy sparametryzowanej typem:

template <typename T>
class VectorTest : public ::testing::Test
{
public:
    using VectorType = std::vector<T>;
    inline static T value_{};

    std::vector<T> vec_{ T{}, T{}, value_ };
};

Następnie definiujemy listę typów, którą chcemy wykorzystać w testach:

using MyListOfTypes = ::testing::Types<char, int, uint64_t>;
TYPED_TEST_SUITE(VectorTest, MyListOfTypes);

Następnie stosujemy makro TYPED_TEST() zamiast TEST_F():

TYPED_TEST(VectorTest, push_back_increases_size)
{
    TypeParam default_value = value_; // TypeParam allows to get the parameter type of a test

    auto size_before = vec_.size();

    vec_.push_back(default_value);

    ASSERT_EQ(size_before + 1, vec_.size());
}

Selekcja testów

Wyłączanie pojedynczego testu

Aby wyłączyć pojedynczy test, wystarczy nazwę tego testu poprzedzić prefiksem DISABLED_.

TEST(TestCase, DISABLED_SimpleTest)

Uruchomienie wybranych testów

Domyślnie Google Test uruchamia wszystkie testy zdefiniowane przez użytkownika.

Jeśli chcemy uruchomić jedynie ich podzbiór, to możemy użyć zmiennej środowiskowej GTEST_FILTER, lub ustawić w przed wywołaniem InitGoogleTest tzw. „filter string”.

int main(int argc, char** argv)
{
    ::testing::GTEST_FLAG(filter) = "*Simple*";
    ::testing::InitGoogleTest(&argc, argv);

    return RUN_ALL_TESTS();
}

Trzecią możliwością jest użycie przełączników linii poleceń.

Przykłady wzorów dopasowania:

./foo_test Has no flag, and thus runs all its tests.
./foo_test --gtest_filter=* Also runs everything, due to the single match-everything * value.
./foo_test --gtest_filter=FooTest.* Runs everything in test case FooTest.
./foo_test --gtest_filter=*Null*:*Constructor* Runs any test whose full name contains either "Null" or "Constructor".
./foo_test --gtest_filter=-*DeathTest.* Runs all non-death tests.
./foo_test --gtest_filter=FooTest.*-FooTest.Bar Runs everything in test case FooTest except FooTest.Bar