fbpx ...

Zaawansowany C++: testowanie, czyli jak nie tracić tygodni na szukanie błędów w kodzie?

Pisanie testów w programowaniu to kluczowy element zapewniający jakość i stabilność aplikacji. Testowanie kodu, refaktoryzacja testów i ich organizacja mogą wydawać się skomplikowane, ale trzymając się kilku prostych zasad, można zapewnić czytelność, utrzymanie i niezawodność testów. W tym artykule przyjrzymy się, jak efektywnie organizować i refaktoryzować testy przy użyciu frameworka GTEST, oraz jakie konwencje są zalecane podczas definiowania testów.

Grupowanie i nazewnictwo testów

Grupowanie testów jest pierwszym krokiem do uporządkowanego i czytelnego zestawu testów. W frameworku GTEST grupujemy testy, nadając im nazwy grup, co działa podobnie jak tagi. Taki system pozwala na łatwiejsze zarządzanie i wyszukiwanie powiązanych testów, co jest niezwykle przydatne w dużych projektach. Warto zwrócić uwagę, aby nazwa grupy testowej była krótka, zwięzła i jasno nawiązywała do testowanego elementu. Dzięki temu, zarówno my, jak i inni programiści, którzy będą pracować z naszym kodem, łatwo zrozumieją, jakie testy są grupowane i dlaczego. Podczas nadawania nazw testom w Catch, zaleca się korzystanie z CamelCase lub Snake_Case. Ważne jest jednak, aby unikać używania spacji, ponieważ nazwy testów nie są przekazywane jako stringi. Konwencje nazewnicze są istotne, gdyż wpływają na czytelność kodu i łatwość jego utrzymania. Google zaleca unikanie Snake_Case, ponieważ podkreślenia mogą być wykorzystywane do innych celów, co może prowadzić do nieporozumień. Zastosowanie odpowiedniej konwencji nazewniczej sprawia, że kod jest bardziej jednolity i profesjonalny.

Struktura testów

Struktura testów w GTEST często opiera się na formacie Given-When-Then, mimo że framework ten nie oferuje bezpośredniego wsparcia dla takich sekcji. Aby zachować przejrzystość kodu, zaleca się używanie komentarzy lub pustych linii do wizualnego oddzielenia sekcji. Taka struktura pomaga w zrozumieniu, co jest przygotowaniem (Given), co jest wykonywane (When), oraz jakie są oczekiwania (Then). Dzięki temu, zarówno pisanie, jak i przeglądanie testów staje się bardziej intuicyjne i zrozumiałe dla programistów.

Test Fixtures

Test Fixture (TEST_F) to zestawy testów, które współdzielą wspólne dane. Pozwalają one na uniknięcie duplikacji kodu i centralizację danych, które są wspólne dla kilku testów. Tworzymy oddzielną strukturę, do której ładujemy wspólne elementy. Dzięki temu, każdy test może korzystać ze wspólnych ustawień, co zwiększa czytelność i utrzymanie testów. Test fixtures umożliwiają także definiowanie funkcji, które będą inicjalizować lub manipulować danymi, co dodatkowo ułatwia zarządzanie stanem testów.

Izolacja testów

Każdy test w GTEST jest izolowany, co oznacza, że konstruktory i destruktory obiektów są wywoływane przed każdym testem. To zapewnia, że stan testów nie będzie przenoszony między nimi, co jest kluczowe dla niezawodności i powtarzalności testów. Izolacja testów jest fundamentalnym aspektem, który pomaga uniknąć subtelnych błędów wynikających z nieoczekiwanych zmian stanu. Dzięki temu możemy być pewni, że każdy test jest niezależny i jego wynik nie jest zależny od innych testów.

Identyfikacja wspólnych danych

Pierwszym krokiem w refaktoryzacji jest zidentyfikowanie testów, które korzystają z tych samych danych. Na przykład, jeżeli mamy dwa testy, które używają tych samych wektorów, warto zastanowić się nad zastosowaniem test fixture, co pozwala na współdzielenie tych danych. Rozpoznanie wspólnych danych pozwala na bardziej efektywne zarządzanie testami i redukuje nadmiarowość kodu. Kiedy już zidentyfikujemy wspólne elementy, możemy stworzyć odpowiednie struktury, które ułatwią ich zarządzanie.

Wstrzykiwanie Zależności (Dependency Injection) i Zasada Odwrócenia Zależności (DIP)

Wstrzykiwanie zależności, czyli Dependency Injection (DI), jest kluczową techniką w programowaniu obiektowym, która pozwala na odseparowanie komponentów systemu, co zwiększa jego elastyczność i testowalność. Jednym z fundamentów dobrych praktyk programistycznych są zasady SOLID, a szczególnie ostatnia litera „D”, która oznacza zasadę odwrócenia zależności (DIP). Zasada ta sugeruje, że moduły wyższego poziomu nie powinny zależeć od modułów niższego poziomu. Implementując DI, można odseparować zależności, co pozwala na łatwiejsze zarządzanie kodem i jego testowanie.

Testowanie kodu - typowe problemy i scenariusze

Problemy z siecią
Jednym z typowych problemów, które mogą pojawić się w trakcie testowania, jest zależność od zewnętrznych usług sieciowych. Na przykład, jeśli mamy funkcję przesyłającą dane przez sieć, może ona napotkać na szereg problemów: brak połączenia, złe ustawienia flag, zerwane połączenia, zapchane bufory czy duże opóźnienia. Wszystkie te scenariusze mogą wpływać na stabilność i przewidywalność testów.

Problemy z bazą danych
Podobne wyzwania pojawiają się, gdy aplikacja korzysta z bazy danych. Problemy takie jak brak połączenia z serwerem bazy danych, duże opóźnienia w odpowiedziach, czy też obciążenie serwera mogą sprawić, że testy stają się niestabilne i trudne do przeprowadzenia. Do tego dochodzą dane, które znajdują się w takiej bazie. Jeśli testy nie czyszczą danych po każdym wykonaniu (także tym, w którym nie przeszły), to każdorazowe ich uruchomienie powoduje pracę na innym zestawie danych. Dlatego, aby uniknąć takich problemów, warto zastosować DI, aby odseparować logikę biznesową od bezpośrednich interakcji z bazą danych.

Funkcje z timeoutem
Kolejnym przykładem są funkcje z timeoutem, które wykonują operacje zdalne i oczekują na odpowiedź przez określony czas. Testy takich funkcji mogą trwać zbyt długo, co nie jest pożądane w testach jednostkowych, które powinny być szybkie i deterministyczne. Ustawienie zbyt krótkiego timeoutu na potrzeby testu również nie jest dobrą praktyką, ponieważ nie odzwierciedla rzeczywistych warunków działania aplikacji.

Mocki i wstrzykiwanie zależności

Mocki są narzędziami, które umożliwiają symulowanie zachowania obiektów w testach jednostkowych. Dzięki mockom możemy ustawiać, jakie wartości mają być zwracane przez metody, sprawdzać, jakie metody zostały wywołane i z jakimi parametrami. Mocki pozwalają na testowanie w izolacji, kontrolowanie zachowania obiektów oraz weryfikowanie interakcji.
Aby zaimplementować mocka, najpierw musimy mieć interfejs lub abstrakcyjną klasę, którą nasz mock będzie implementować. To pozwala na odseparowanie logiki biznesowej od konkretnej implementacji, co jest zgodne z zasadą odwrócenia zależności (DIP). Tworzenie mocków jest procesem, który może różnić się w zależności od używanej biblioteki, ale w Google Mock jest to dość intuicyjne. Biblioteka ta pozwala na definiowanie zachowania metod, ustawianie oczekiwań co do wywołań metod oraz weryfikowanie tych wywołań. Mocki w Google Mock są tworzone na podstawie interfejsów, co pozwala na łatwe zastępowanie rzeczywistych implementacji w testach.
Mocki mogą być używane do symulowania różnych scenariuszy w testach. Na przykład, możemy używać mocków do symulowania odpowiedzi z serwera sieciowego, odpowiedzi z bazy danych, czy zachowania funkcji z timeoutem. Dzięki temu możemy przetestować, jak nasz kod zachowuje się w różnych sytuacjach bez konieczności polegania na rzeczywistych zależnościach.

Czym jest Test-Driven Development (TDD)?

Test-Driven Development (TDD) to podejście do programowania, które kładzie duży nacisk na pisanie testów przed implementacją kodu. Jest to skuteczna metoda zapewniająca, że kod działa zgodnie z oczekiwaniami. TDD składa się z trzech głównych faz: Red, Green, Refactor.
Łatwiej jest rozpocząć pracę z TDD, gdy mamy już gotową część aplikacji lub przynajmniej podstawowy szkielet. W istniejących projektach mamy już pewne wymagania i struktury, co ułatwia dodawanie nowych funkcjonalności. Jednakże, kiedy tworzymy projekt od zera, musimy najpierw utworzyć podstawowy szkielet aplikacji, który będzie zawierał główną aplikację oraz testy. Na początek warto uruchomić proste testy, aby upewnić się, że środowisko testowe działa poprawnie.

Fazy:
🔴 RED – Pisanie testu, który nie przechodzi. Na przykładzie gry w szachy, zaczynamy od napisania testu, który sprawdzi, czy możemy umieścić pionka na szachownicy. W tej fazie test zawsze będzie nieudany, ponieważ jeszcze nie zaimplementowaliśmy funkcjonalności.
🟢 Green – Implementacja kodu, aby test przeszedł. W tej fazie piszemy minimalną ilość kodu potrzebną do przejścia testu. Dla przykładu, implementujemy metody pozwalające na umieszczanie pionków na szachownicy i sprawdzanie ich położenia.
🔄 Refactor – Optymalizacja kodu bez zmiany jego funkcjonalności. Po przejściu testu możemy skupić się na poprawie jakości kodu, usuwaniu duplikacji i refaktoryzacji.

Zasady TDD:
1. Pisanie jednego testu na raz: w TDD ważne jest, aby pisać jeden test na raz, a następnie przejść przez pełny cykl Red, Green, Refactor.
2. Kompilacja testu: test musi się skompilować przed przystąpieniem do implementacji kodu. W ten sposób upewniamy się, że nasze testy są poprawne.
3. Minimalna implementacja: w fazie Green piszemy minimalną ilość kodu potrzebną do przejścia testu. Dopiero później refaktoryzujemy kod.

Podsumowanie

Efektywne testowanie i refaktoryzacja kodu to kluczowe aspekty tworzenia niezawodnego oprogramowania. Organizacja testów w frameworku GTEST poprzez grupowanie, stosowanie odpowiednich konwencji nazewniczych oraz korzystanie z test fixtures i mocków znacząco ułatwia zarządzanie i utrzymanie kodu testowego. Wstrzykiwanie zależności oraz zasada odwrócenia zależności (DIP) zwiększają elastyczność i testowalność aplikacji. Test-Driven Development (TDD) pomaga programistom pisać kod, który spełnia oczekiwania, a fazy Red, Green i Refactor prowadzą do systematycznego i przemyślanego rozwoju oprogramowania.

Chcesz wiedzieć więcej? Sprawdź naszą playlistę o testowaniu, albo dołącz do kursu i testuj jak zawodowiec 🧪

ninjaletter

A może Ninjaletter?

Chcesz wiedzieć, co słychać w C++ i nie tylko? Zapisz się na Ninjaletter i otrzymuj od nas co miesiąc dawkę wartościowych treści o C++ i zadania rekrutacyjne. Do tego dorzucamy darmowe materiały, spoilery o nowych kursach, specjalne promocje dla ninjaletterowiczów i wiele, wiele innych. To co, skusisz się?

Łukasz Ziobroń

Łukasz Ziobroń

Zmieniam ludzi w prawdziwych programistów. W nauczaniu stosuję grywalizację, andragogikę i neurodydaktykę.

Najnowsze artykuły

docker

Narzędzia programisty: Docker w skrócie

Co wspólnego ze sobą mają ogry, cebula i Docker? Poznaj podstawy Dockera i dowiedz się, jak może przyspieszyć Twoją codzienną pracę. Odkryj, dlaczego warto go mieć w swoim arsenale programisty.

Czytaj »
good programming practices

Good programming practices – Coding Dojo

Training in a form of Coding Dojo. Participants start with a code review of a small application. They note down their comments. After that, the trainer presents bad and good programming practices. Participants discuss what can be applied in a reviewed code and start fixing it in a form of Coding Dojo.

Czytaj »

Popular C++ Idioms – Coding Dojo

The training starts with a code review of a small application (pre-work). Participants note their thought and discuss their findings in groups. Then popular C++ idioms are presented (the concept and some code) – about 15-20 minutes each. After that participants need to use some of the idioms in a reviewed application code.

Czytaj »
performance optimisations

Performance optimisations

This training is about writing more robust C++ code and algorithms with the help of CPU caches and a compiler. Benchmarking tools are used to show performance gains.

Czytaj »
ninjaletter

Już uciekasz?

Zanim to zrobisz, zapisz się na Ninjaletter, aby wiedzieć, co piszczy w C++. 

Informujemy, iż w celu realizacji usług dostępnych w naszym serwisie, optymalizacji jej treści, dostosowania strony do Państwa indywidualnych potrzeb oraz wyświetlania, personalizacji i mierzenia skuteczności reklam w ramach zewnętrznych sieci reklamowych korzystamy z informacji zapisanych za pomocą plików cookies na urządzeniach końcowych użytkowników. Pliki cookies można kontrolować za pomocą ustawień swojej przeglądarki internetowej. Dalsze korzystanie z naszego serwisu, bez zmiany ustawień przeglądarki internetowej oznacza, iż użytkownik akceptuje stosowanie plików cookies. Więcej informacji zawartych jest w polityce prywatności serwisu.