C++ Testy jednostkowe: stawianie gTest

Mały wstęp do tematu testów jednostkowych. Trudno docenić to jak przydatne są testy jednostkowe zaczynając pisać projekt od podstaw. O wiele łatwiej jest docenić ich wage modyfikując kod, którego autor dawno zapomniał o jego istnieniu..

Na początku zastanawiamy się jak poprawnie skorzystać z istniejącego kodu. Następnie gdy myślimy, ze go rozumiemy, staramy się zmodyfikować go tak, by spełniał nowe oczekiwania. Prędzej czy później zastanawiamy się czy przypadkiem nasze zmiany nie zaburzają poprzedniego sposobu działania aplikacji. Co gorsza, czasem trafiamy na jakieś wyjątkowe zachowanie i trudno jest nam zdecydować czy to w jaki sposób kod działał do tej pory było zamiarem autora czy może niedopatrzeniem. Posiadając testy jednostkowe dostajemy odpowiedzi na te pytania. Widzimy w nich przykłady użycia zademonstrowanych funkcjonalności, przewidziane przez autora warianty oraz możemy w krótkim czasie zweryfikować, czy nasze zmiany nie zaburzają istniejącego świata. Czym są same testy jednostkowe w praktyce? Odpowiedź na to pytanie powinna pojawić się na końcu artykułu wraz z przykładem.

Niezbędne narzędzia.

W dzisiejszym artykule przedstawię dwie rzeczy:

  1. Przygotowanie gTest do pracy z nowym projektem
  2. Stworzenie prostego testu dla nowego projektu

Na ten moment tyle wystarczy. Nie marnujemy czasu, więc do rzeczy:

Przygotowanie gTest.

Na starcie stworzyłem projekt samej aplikacji. Nazwałem ją SMSITBudgetClient. Projekt póki co zostawiamy w spokoju. 
W tej samej solucji stworzyem drugi projekt, który nazwałem SMSITBudgetClient_uTests. W ramach tego projektu będę realizował testy jednostkowe.
Aby dodać nowy projekt, klikamy PPM na solucję->ADD->New Project. Po stworzeniu projektu klikamy na niego PPM i wybieramy "Set as StartUp Project"

Zaczynamy od zainstalowania google test.
Aby tego dokonać najpierw go pobierzemy: https://github.com/google/googletest

Ściągnięty plik wypakowujemy w wygodnej dla nas lokalizacji.
Wchodzimy kolejno do katalogów googletest/msvc. Tak, dobrze się domyślasz- katalog MSVC został stworzony z myślą o użytkownikach microsoft visual studio. Uruchamiamy plik gtest.sln- najpewniej dostaniemy informacje o potrzebie migracji solucji do nowszej wersji- zróbmy to. Ok po tym zabiegu należy zbudować nasz projekt (w programie Visual Studio klikamy Build->Build solution). Operacja budowania powinna zakończyć się sukcesem. Pamiętajmy aby zbudować wersję zarówno release jak i debug.

W folderze, w którym uruchamialiśmy solucję pojawiły się katalogi: debug oraz release w zależności od tego jaką wersję zbudowaliśmy. W tych katalogach możemy między innymi znaleźć zbudowane biblioteki gtest.lib/gtestd.lib oraz pliki *.exe.

Jedna ważna rzecz do zauważenia. Po zbudowaniu naszego gtest, kliknijmy PPM->Properties->Configuration Properties->C/C++/Code Generation i w tym okienktu zwróćmy uwagę na zawartość przy polu Runtime Library. Najpewniej jest to "Multi-threaded Debug (/MTd)" dla wersji debug oraz "Multi-threaded (/Mt)" dla wersji release. To co musisz wiedzieć, to fakt, że każdy projekt, w którym będziesz chciał użyć tych libów, musi mieć podobne ustawienia :)

OK, googletest powinien być gotowy do użycia. Pora zaprzęgnąć narzędzie do pracy.

Nasz projekt do testów jednostkowych musi widzieć biblioteki gtest. W tym celu klikamy PPM na nasz projekt, potem properties, a następnie zwracamy uwagę na pole, przy którym zaznaczyłem czerwoną kropkę na zrzucie niżej. Dodajemy tam ścieżkę do folderu include z katalogu google test. Pamiętaj by wykonać tę operację zarówno dla wersji release jak i debug ;) 

MSVSWindow

Następnie instrukcję main zamieniamy na podobną do tej:

#include "stdafx.h"
#include <gtest\gtest.h>

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

 

I teraz chwila próby. Naciskamy build.

error LNK2019: unresolved external symbol "public: static class testing::UnitTest * __cdecl testing::UnitTest::GetInstance(void)" (?GetInstance@UnitTest@testing@@SAPAV12@XZ) referenced in function "int __cdecl RUN_ALL_TESTS(void)" (?RUN_ALL_TESTS@@YAHXZ)

Ok, zapomnieliśmy zamieścić zbudowane biblioteki. Pamiętasz, budowaliśmy je za pomocą solucji googletest. PPM na projekcie->properties->Configuration Properties->Linker->General w polu Additional Library Directories musimy dodać adres do naszego wygenerowanego liba. Dla debug dodajemy "X:\googletest\msvc\gtest\Debug" (gdzie x to ścieżka do googletest).
Następnie w zakładce niżej "Input" dla pola Additional Dependencies dodajemy nasze pliki. Dla debug "gtestd.lib" oraz odpowiednio dla release "gtest.lib"
Budujemy jeszcze raz ;)

error LNK2038: mismatch detected for 'RuntimeLibrary': value 'MTd_StaticDebug' doesn't match value 'MDd_DynamicDebug' in Project_name

Ok kolejny problem. Pamiętasz jak zaznaczaliśmy, że każdy projekt, który będzie używał naszych libów, musi mieć takie same opcje odnośnie Multi-threaded debug? Właśnie. Dlatego nie działa :) Ustawmy odpowiednie opcje dla wersji release i debug. Zmieniamy je tam, gdzie wcześniej je sprawdzaliśmy. (PPM->Properties->Configuration Properties->C/C++/Code Generation)

Klikamy buduj i... U MNIE DZIAŁA ;) Jeżeli u Ciebie nie, to niestety google i szukaj, na stacku pewnie ktoś już miał podobnie.

Testowy test

Ok napiszmy mały test tego czy faktycznie testy realizowane są poprawnie i czy nadal nic nie wybucha. 

#include "stdafx.h"
#include "gtest\gtest.h"

bool isEven(int number) { return number % 2 == 0; }
TEST(isEvenTest, even_argument)
{
	EXPECT_TRUE(isEven(2));
}
TEST(isEvenTest, odd_argument)
{
	EXPECT_FALSE(isEven(3));
}

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

output:

 cmd1

Program uruchamiamy klikając ctrl+F5 (wtedy konsola nie znika tak szybko ;)). OK działa. W naszym kodzie chcemy miec funkcję, która sprawdzi czy liczba podana jako argument funkcji jest parzysta. Najprostrze testy jakie mogą sprawdzać działanie takiej funkcji, zostały zamieszczone w kodzie. Widzimy konstrukcję TEST(A,B). 'A' możemy użyć jako nazwę grupy testów, czyli np nazwa testowanego komponentu, natomiast 'B' jako konkretny test. Dla przykładu będziemy testowć funkcję IsEven więc A wyznaczamy jako isEvenTest, natomist B, które opisuje dokładnie przypadek testowy: even_argument lub odd_argument. Dzięki temu na etapie testowania możemy łatwo zobaczyć co testujemy w danym momencie. Nastęnie piszemy funkcję, która powinna spełnić te dwa testy. Funkcja IsEven na tym etapie spełnia dwa testy, więc odpalamy nasz program i sprawdzamy jak działa.

Tutaj możemy powiedzieć czym w zasadzie jest test- jest sprawdzeniem, czy określone warunki są spełnione. Dla przykładu nasze testy sprawdzają, czy wynik działania funkcji ma wartość true (expect true), lub false (expect false). Zademonstrowana forma testu jest najprostrzą jaką znam- dzięki temu powinna być jasna. Jako arugment testu wywołujemy testowaną funkcję. test EXPTECT_TRUE spodziewa się, że wartość zwrócona z testowanej funkcji będzie wynosiła 'true'. Jeżeli warunek ten będzie spełniony test zostanie zaliczony, w innym przypadku zasygnalizowany zostanie błąd. Stąd nazwa test jednostkowy. W ty przypadku nie testujemy dużych funkcjonalności, czy całych modułów. Zadaniem testów jednostkowych jest przeterstowanie jednego małego wycinka kodu. Dzięki temu wiemy, w którym dokładnie miejscu występują problemy, a w których wszystko działa dobrze.

I na tym chciałbym dzisiaj skończyć artykuł. Najważniejsze, że narzędzie działa- możesz się nim pobawić sprawdzając jak zachowa się np w przypadku niezaliczenia testu. W następnym artykule przejdziemy do testowania prostej klasy używając fixture. Dowiesz się, w jaki sposób testować kod nieco bardziej złożony niż proste funkcje :) Do zobaczenia!

 

 


źrodła:
https://blog.jetbrains.com/rscpp/unit-testing-google-test/
http://www.bogotobogo.com/cplusplus/google_unit_test_gtest.php
http://www.ibm.com/developerworks/aix/library/au-googletestingframework.html