Testy parametryzowane w Google Test – praktyczny przewodnik
Testy parametryzowane stanowią kluczowe narzędzie w frameworku Google Test (GTest), umożliwiające wykonywanie tych samych przypadków testowych z różnymi zestawami danych wejściowych. Technika ta eliminuje redundancję kodu, zwiększa pokrycie testowe i usprawnia weryfikację algorytmów działających na zróżnicowanych danych. W przeciwieństwie do standardowych testów jednostkowych, gdzie każdy scenariusz wymaga oddzielnej implementacji, parametryzacja pozwala na centralne zarządzanie danymi testowymi i automatyczne generowanie przypadków.
Podstawy testów parametryzowanych w GTest
Aby utworzyć testy parametryzowane, należy zdefiniować klasę fixture dziedziczącą po testing::TestWithParam<T>, gdzie T oznacza typ parametrów testowych. Klasa ta pełni rolę kontenera dla logiki inicjalizacyjnej i sprzątającej, identycznie jak w tradycyjnych fixture’ach. Różnica polega na dostępie do parametrów poprzez metodę GetParam() w ciele testów.
#include <gtest/gtest.h>
class ExampleFixture : public testing::TestWithParam<int> {
protected:
void SetUp() override { /* inicjalizacja */ }
void TearDown() override { /* sprzątanie */ }
};
Do definiowania samych testów wykorzystuje się makro TEST_P, którego składnia odzwierciedla strukturę TEST_F, lecz z dodatkowym dostępem do parametrów:
TEST_P(ExampleFixture, TestCase) {
int param = GetParam();
ASSERT_GT(param, 0); // Przykładowa asercja
}
Kluczowym etapem jest instancjacja zestawu testowego z konkretnymi parametrami za pomocą makra INSTANTIATE_TEST_SUITE_P. Składnia:
INSTANTIATE_TEST_SUITE_P(
NazwaInstancji, // Unikalny prefiks dla wyników
ExampleFixture, // Klasa fixture
testing::Values(1, 2, 3, 4) // Generator parametrów
);
W wyniku tego zestaw testów zostanie wykonany dla każdej wartości w Values, a w wynikach pojawią się nazwy postaci NazwaInstancji/ExampleFixture.TestCase/0, /1, itd.
Typy generatorów parametrów
GTest oferuje kilka wbudowanych generatorów:
- Values(v1, v2, …, vN) – jawna lista wartości;
- Range(start, end, step) – sekwencja liczb (domyślnie step=1);
- ValuesIn(container) – wartości z kontenera (np. vector, tablica);
- Bool() – wartości
falseitrue; - Combine(g1, g2, …) – produkt kartezjański generatorów (wymaga krotki
std::tuple).
Przykład użycia ValuesIn z wektorem:
std::vector<std::string> inputs = {"cat", "dog"};
INSTANTIATE_TEST_SUITE_P(
AnimalTests,
MyFixture,
testing::ValuesIn(inputs)
);
Dla generatora Combine należy zadeklarować parametr jako krotkę:
class AdvancedFixture : public testing::TestWithParam<std::tuple<int, bool>> {};
TEST_P(AdvancedFixture, CombinedTest) {
auto [num, flag] = GetParam();
// ...
}
INSTANTIATE_TEST_SUITE_P(
CombineDemo,
AdvancedFixture,
testing::Combine(
testing::Values(1, 2),
testing::Bool()
)
);
Zaawansowane techniki parametryzacji
Niestandardowe nazwy testów
Domyślne nazwy generowane przez GTest (np. /0, /1) bywają nieczytelne. Aby temu zaradzić, można dostarczyć funkcję generującą nazwy jako czwarty argument INSTANTIATE_TEST_SUITE_P.
std::string CustomName(const testing::TestParamInfo<std::tuple<int, bool>>& info) {
auto [num, flag] = info.param;
return fmt::format("{}_Flag{}", num, flag);
}
INSTANTIATE_TEST_SUITE_P(
CustomNamedTests,
AdvancedFixture,
testing::Combine(/*...*/),
CustomName // Wskaźnik do funkcji
);
Wynik: CustomNamedTests/AdvancedFixture.CombinedTest/0_FlagTrue
Łączenie parametryzacji z fixture’ami
Parametryzację można łączyć z fixture’ami klasycznymi, tworząc wielowarstwowe struktury testowe. Przykładowo, gdy potrzebne są zarówno różne typy danych, jak i wartości:
template <typename T>
class TypedFixture : public testing::Test {};
TYPED_TEST_SUITE_P(TypedFixture); // Rejestracja szablonu
TYPED_TEST_P(TypedFixture, TypeTest) {
TypeParam value{};
// ...
}
REGISTER_TYPED_TEST_SUITE_P(TypedFixture, TypeTest); // Rejestracja testów
using Types = testing::Types<int, float, double>;
INSTANTIATE_TYPED_TEST_SUITE_P(NumericTypes, TypedFixture, Types);
Dodanie parametryzacji wartościowej wymaga rozszerzenia fixture’a o TestWithParam:
template <typename T>
class ComboFixture : public testing::TestWithParam<std::tuple<T, std::string>> {};
TEST_P(ComboFixture, AdvancedTest) {
auto [value, str] = GetParam();
// ...
}
Testowanie interfejsów HAL
W środowisku Android (VTS), testy parametryzowane wykorzystuje się do weryfikacji implementacji sprzętowych (HAL). Przykładowo, test USB HAL:
class UsbHidlTest : public testing::TestWithParam<std::string> {
void SetUp() override {
usb = IUsb::getService(GetParam()); // Pobranie instancji
}
sp<IUsb> usb;
};
TEST_P(UsbHidlTest, SetCallback) {
// ... test z użyciem 'usb'
}
INSTANTIATE_TEST_SUITE_P(
PerInstance,
UsbHidlTest,
testing::ValuesIn(android::hardware::getAllHalInstanceNames(IUsb::descriptor))
);
Generator getAllHalInstanceNames automatycznie zbiera dostępne implementacje interfejsu.
Przykłady praktyczne
Testowanie roku przestępnego
Rozważmy test logiki określającej lata przestępne. Parametry obejmują rok i oczekiwany wynik:
struct LeapYearParams {
int year;
bool expected;
};
class LeapYearTest : public testing::TestWithParam<LeapYearParams> {};
TEST_P(LeapYearTest, ValidityCheck) {
auto params = GetParam();
ASSERT_EQ(isLeapYear(params.year), params.expected);
}
INSTANTIATE_TEST_SUITE_P(
LeapYearTests,
LeapYearTest,
testing::Values(
LeapYearParams{2020, true},
LeapYearParams{1900, false},
LeapYearParams{2000, true}
)
);
Testowanie funkcji matematycznych
Dla funkcji addOne test z użyciem krotek:
TEST_P(AddOneTests, CheckResults) {
auto [input, expected] = GetParam();
ASSERT_EQ(addOne(input), expected);
}
INSTANTIATE_TEST_SUITE_P(
AddOneTests,
AddOneTestsFixture,
testing::Values(
std::make_tuple(1, 2),
std::make_tuple(-3, -2)
)
);
Rozwiązywanie problemów i najlepsze praktyki
Ograniczenia i pułapki
- Struktury z wiązaniami w C++17 – w lambda-generatorach nazw nie można używać strukturalnych wiązań (
auto [a,b] = ...); - Wskaźniki jako parametry – gdy
Tjest wskaźnikiem, użytkownik musi zarządzać cyklem życia danych; - Unikanie
_w nazwach – GTest wymaga, aby nazwy fixture’ów i testów były poprawnymi identyfikatorami C++ bez znaku podkreślenia.
Zalecane praktyki
- Izolacja parametrów – każdy test powinien być niezależny; unikać współdzielonego stanu modyfikowalnego między instancjami;
- Czytelność wyników – generuj opisowe nazwy testów poprzez funkcje customowe;
- Minimalizacja zakresu – parametryzuj tylko testy wymagające wielu danych. Pozostałe implementuj jako
TEST_F; - Testowanie skrajnych przypadków – wartości brzegowe (np.
0,INT_MAX, puste stringi) zawsze powinny znaleźć się w parametrach.
Podsumowanie
Testy parametryzowane w GTest stanowią potężny mechanizm zwiększający efektywność testowania. Poprzez oddzielenie danych testowych od logiki testów, pozwalają na łatwe dodawanie przypadków i utrzymanie czytelności kodu. Połączenie generatorów (Values, Range, Combine) z możliwością definiowania własnych formatterów nazw daje elastyczność niezbędną w złożonych projektach.
W środowiskach produkcyjnych, szczególnie w testach warstw sprzętowych (np. Android VTS), technika ta okazuje się nieoceniona dla zapewnienia kompletności walidacji. Przestrzeganie zasad izolacji testów i dbałość o czytelność wyników stanowią klucz do sukcesu w długoterminowym utrzymaniu testów parametryzowanych.
Uwaga: Wszystkie przykłady kodu pochodzą z dokumentacji GTest i zmodyfikowanych przypadków użycia z podanych źródeł. Wdrożenie tych technik wymaga aktualnej wersji Google Test (min. 1.10) i wsparcia C++14.
