Zmiany wprowadzane w tradycyjnych programach często psują ich funkcjonalność, powodując przy tym ogromne szkody w działalności całego systemu.
Chcąc uniknąć takiej sytuacji, należy wykorzystywać rozwój oparty na testach (TDD – ang. Test-driven Development) dzięki narzędziu ABAP Unit. W tym artykule wyjaśnię, czym jest TDD i jak je wdrożyć w ABAP-ie.
W tradycyjnym procesie programistycznym po stworzeniu nowego programu lub zmianie istniejącego następnym krokiem jest wykonanie podstawowych testów. W rzeczywistości jednak często brakuje czasu na ten etap prac i jest on pomijany, często z katastrofalnymi skutkami. Przeciwieństwem takiego procesu jest TDD, polegający na pisaniu testów przed utworzeniem nowej funkcjonalności bądź zmiany istniejącej.
W artykule skupię się na działaniu framework’a ABAP Unit i na tym, jak dodawać testy jednostkowe do istniejącego kodu, czy to proceduralnego czy obiektowego. Programiści, którzy piszą nowy kod lub tworzą zmiany w kodzie istniejącym, powinni zacząć swoją pracę od napisania testu jednostkowego przed dokonaniem zmian w kodzie. Może to się wydawać dziwne, ponieważ tworzysz test jednostkowy do kodu, który jeszcze nie istnieje. Ale o to właśnie chodzi. Najpierw udowadniasz, że napisany kod nie robi tego co tak naprawdę powinien, poprzez napisanie testu jednostkowego. Następnie tworzysz kod, aż w 100% będziesz pewny, że problem został rozwiązany.
W normalnym procesie developmentu piszesz kod, aby rozwiązać problem i dopóki nie zrobisz testu ręcznego, nie jesteś w 100% pewien czy napisałeś kod poprawnie, czy też nie. TDD zapobiega takiej sytuacji przebiegając według określonego cyklu:
- Napisanie przypadku testowego.
Na tym etapie powinniśmy wiedzieć, co chcemy przetestować i czego oczekujemy od testowanego funkcjonalności. - Uruchomienie napisanego testu.
Uruchomienie testu i oczekiwanie na błąd kompilacji, ponieważ nie istnieje kod, który chcemy testować. - Napisanie minimalnego kodu.
Stworzenie minimalnego kodu odpowiedzialnego za funkcjonalność, dzięki któremu po uruchomieniu testu przejdzie w stan „zielony”. - Refaktoryzacja napisanego kodu.
Przeprowadzamy refaktoryzację kodu produkcyjnego i kodu testowego. - Uruchomienie wszystkich testów.
Cykl TDD się zamyka i powtarzamy go, dopóki nie napiszemy wszystkich testów pokrywających logikę biznesową.
Definicja klasy testowej
Aby zdefiniować klasę testową, musisz najpierw mieć zdefiniowaną klasę globalną, używając do tego transakcji SE24 albo SE80. Po stworzeniu klasy globalnej przejdź do menu: Skok do → Lokalne definicje/implementacje → Lokalne klasy testowe (z ang. Goto → Local Definitions/Implementations → Local Test Class).
W klasie testowej musisz zdefiniować kilka rzeczy:
- Włączenie testowania prywatnych metod.
- Ustalenie ogólnych ustawień definicji klasy testowej.
- Deklaracja definicji danych.
- Konfiguracja testów jednostkowych.
- Definicja rzeczywistych metod testowania.
- Implementacja metod pomocniczych.
Włączenie testowania prywatnych metod
Aby włączyć testowanie prywatnych metod należy dodać następujący kod:
W powyższym przykładzie klasa ZCL_RANGE jest klasą globalną, którą będzie testowana przez lokalną klasę LCL_RANGE_TEST_CLASS. Włączenie funkcjonalności testowania metod prywatnych odbywa się za pośrednictwem słów kluczowych LOCAL FRIENDS nazwa_klasy_testowej.
Ustalenie ogólnych ustawień definicji klasy testowej
Pierwsza linijka mówi, że jest to klasa testowa. Następnie definiujemy RISK LEVEL (poziom ryzyka) – rozróżniane
są trzy: CRITICAL, DANGEROUS i HARMLESS. Ostatnią właściwością jest DURATION (czas trwania testu), gdzie do wyboru są trzy poziomy: LONG, MEDIUM oraz SHORT.
Deklaracja definicji danych
Kontynuując definicję klasy testowej dochodzimy do deklaracji danych. Pierwszą i najważniejszą zmienną jaką deklarujemy jest zmienna do przechowywania instancji klasy, która jest testowana.
Definicja metody SETUP
Pierwszą metodą, którą należy zawsze zdefiniować jest prywatna metoda SETUP. Odpowiedzialna jest za resetowanie stanu systemu, tak aby każda metoda testowa zachowywała się jakby była pierwszą uruchomioną metodą testową. Dlatego każda z globalnych zmiennych musi zostać wyczyszczona lub ustawiona na określoną wartość, a testowana klasa musi zostać utworzona od nowa. Pozwala to uniknąć tzw. sprzężenia czasowego (wynik jednego testu może mieć wpływ na wynik drugiego testu).
Definicja rzeczywistych metod testowania
Po definicji metody SETUP następnym krokiem jest definicja rzeczywistych metod testowania. Słowa kluczowe FOR TESTING po nazwie metody informują, że ta metoda będzie uruchamiana za każdym razem, kiedy programista uruchomi test.
Implementacja metod pomocniczych
Ostatnim krokiem jest implementacja metod pomocniczych (bez słów kluczowych FOR TESTING).
Są to zwyczajne metody prywatne wywoływane przez metody testowe. Celem metod pomocniczych jest wykonanie zadań niższego poziomu dla jednej lub więcej metod testowych.
Struktura GIVEN/WHEN/THEN
Kod metody testującej powinien być napisany we wzorcu GIVEN/WHEN/THEN. GIVEN opisuje stan tuż przed testem, który ma zostać wykonany. W tej sekcji również określane są parametry wejściowe testu. W sekcji WHEN testowana jest funkcjonalność. W ostatniej części THEN sprawdzane są warunki (wywoływane metody ASSERT).
Test Doubles
Założeniem Test Doubles jest podmienić w trakcie trwania testów zależności testowanej klasy czymś, nad czym mamy pełną kontrolę. Warto zdecydować się na taki manewr w momencie, gdy nie możemy (albo z jakiś powodów nie chcemy) skorzystać z prawdziwych zależności (w obiekcie zależnym „dużo się dzieje”/posiada skomplikowaną logikę). Tak naprawdę te zależności są w rzeczywistości nieszkodliwymi duplikatami wykorzystywanymi tylko i wyłącznie do celów testowych.
Kiedy mówimy o testowaniu podwójnym używamy terminów: stub, spy, mock object, dummy object, fake object. Są to rodzaje Test Doubles, które są wykorzystywane w zależności od potrzeb.
- Stub – wykorzystywany jest w sytuacji, kiedy obiekt klasy testowej jest zależny od zwracanych parametrów przez metodę obiektu dublowanego.
- Spy – wykorzystywany jest w sytuacji, kiedy chcemy mieć pewność, że dana metoda innego obiektu została wywołana przez klasę testową z oczekiwanymi parametrami, nie spodziewając się odpowiedzi. Jest to tzw. punkt obserwacyjny, który weryfikuje nasze założenia.
- Mock Object – wykorzystujemy ten typ, gdy zależy nam na konkretnej odpowiedzi w przypadku otrzymania konkretnych wartości.
- Fake Object – dublowane obiekty, które zawierają mniej skomplikowana logikę biznesową w porównaniu do oryginalnych obiektów.
- Dummy Object – wykorzystywane w sytuacji, kiedy chcemy przekazać samą instancję obiektu.
Implementacja Test Doubles
Na potrzeby przykładu stworzyłem dwie klasy oraz jeden interfejs, który jest odpowiedzialny za pobieranie aktualnego kursu walut. Użytkownik podaje walutę źródłowa i docelową, a interfejs zwraca odpowiedni kurs. Pierwsza klasa jest odwzorowaniem banknotu. Zawiera w sobie dwa atrybuty AMOUNT i CURRENCY oraz metody GET dla tych atrybutów. Druga klasa jest odwzorowaniem portfela. Zawiera atrybuty BANKNOTE i RATE_PROVIDER oraz metody: ADD_BANKNOTE, SET_RATE_PROVIDER oraz GET_AMOUNT. Stworzyłem klasę testową o następującej definicji oraz implementacji.
Aby test klasy ZCL_DEMO_WALLET został poprawnie napisany musiałem wykorzystać Test Doubles. Stworzyłem instancję obiektu dublowanego, który implementuje interfejs ZIF_DEMO_RATE_PROVIDER (linia 29). W naszym przykładzie metodą do dublowania będzie GET_CURRENCY_RATE. Może się to wydawać dziwne, ale na początku nie wywołujemy tej metody, lecz ustawiamy wartość jaką ona zwróci, a następnie jest ona wywoływana (linie 30-33).
W teście wykorzystałem stub-a, ponieważ zwracana wartość przez metodę GET_TOTAL_AMOUNT jest zależna od obiektu RATE_PROVIDER. Jeśli testy zostały wykonane prawidłowo, wówczas powinny być oznaczone na zielono. Na poniższym obrazku znajduje się wynik testu:
Uruchamianie testów jednostkowych
Aby uruchomić testy jednostkowe, należy przejść do transakcji SE80. Następnie należy odnaleźć testowaną klasę oraz kliknąć na niej PPM i wybrać: Wykonanie -> Testy modułowe z -> Pomiar pokrycia:
Podsumowanie
Testowanie jednostkowe wcale nie jest łatwe, wręcz przeciwnie. Używając funkcjonalności
z pełnym pokryciem testowym i korzystając z metodologii TDD przy ich tworzeniu, mamy dużą lepszą kontrolę nad utrzymaniem i rozwojem kodu. Wprowadzając dowolne zmiany czy przeprowadzając refaktoryzację kodu, w ciągu kilku sekund dowiesz się, czy zepsułeś jakąkolwiek istniejącą funkcjonalność. Kiedy zaczniesz korzystać z TDD, przekonasz się, że cykl ten przebiega w kilku krokach. Musisz jedynie poświęcić trochę czasu i nabrać wprawy, a w przyszłości testy jednostkowe staną się nieodzownym elementem Twojej pracy.
Great content, appreciate your hard work. And as usual Unit testing is actually hard and it takes a lot of time. And yes TDD method is the best way to go with as explained here at https://eduhelphub.com/. Thanks again.
Świetna treść, doceń swoją ciężką pracę. I jak zwykle testowanie jednostkowe jest naprawdę trudne i zajmuje dużo czasu. I tak, metoda TDD jest najlepszym sposobem, jak wyjaśniono tutaj na https://eduhelphub.com/. Dzięki jeszcze raz.