Powrót do: Podstawy Testów Automatycznych w Selenium i Python cz. 5 – Profesjonalna konfiguracja projektu
Trochę teorii o dekoratorach
Czym jest dekorator?
Jest to wzorzec projektowy, który służy do rozszerzania funkcjonalności istniejących obiektów (funkcji, metod, klas). Dekorator “opakowuje” daną funkcję, dzięki temu nie modyfikujemy jej działania, ale otrzymujemy możliwość wykonania dodatkowego kodu. Może to zostać zrobione przed albo po wykonaniu funkcji dekorowanej. Za pomocą dekoratorów możemy także zmodyfikować wartości, jakie trafią do “opakowanej funkcji”. Dodatkowo dany dekorator można używać do opakowania różnych funkcji, co pozytywnie wpływa na utrzymywalność kodu.
Pierwszy raz z opakowaniem funkcji zetknęliśmy się przy okazji EventFiringWebDriver w lekcji Akcja na żądanie – EventFiringWebDriver. Opakowaliśmy tam naszego drivera
, dzięki czemu zyskał on nowe możliwości i funkcje. Pozwoliło to nam na wykonanie naszego kodu do tworzenia zrzutów ekranu podczas wystąpienia wyjątku.
Kodzimy
Zacznijmy od prostego przykładu. Powiedzmy, że mamy funkcję print_hello_jaktestowac
, która ma proste zadanie:
Wypisywać (jak się już pewnie domyślasz) nieskomplikowany napis ‘Hello jaktestowac!’. Całość, wraz z wywołaniem, będzie miała postać:
def print_hello_jaktestowac(): print('Hello jaktestowac!') print_hello_jaktestowac()
Wrzucamy ten kod do scratches (Otwieranie widoku Scratches i tworzenie pliku Scratches) i uruchamiamy kod. Nic nadzwyczajnego się nie dzieje – ostatnia linijka z kodem print_hello_jaktestowac()
jest odpowiedzialna za wywołanie funkcji. Funkcja print_hello_jaktestowac()
z kolei uruchamia funkcję print()
ze stringiem ‘Hello jaktestowac!’. Po uruchomieniu kodu finalnie otrzymujemy na konsoli tekst:
Hello jaktestowac!
Nic skomplikowanego.
Co jeśli chcielibyśmy dodać kod, który zawsze wywoła się tuż przed wywołaniem naszej funkcji.?
Dodajmy przed wypisaniem tekstu prostą informację, która wyświetli nam aktualny czas.
import datetime print(datetime.datetime.now())
Tak, tak to nie jest pomyłka – pakiet datetime zawiera klasę datetime
i wiemy, wygląda to conajmniej dziwnie 😀
Ok nic trudnego:
import datetime def print_hello_jaktestowac(): print(datetime.datetime.now()) print('Hello jaktestowac!') print_hello_jaktestowac()
Gdy nie możemy modyfikować funkcji
Wprowadzimy jednak małe zastrzeżenie. Nie chcemy modyfikować samej funkcji tylko rozszerzyć jej działanie. Dlaczego bez modyfikacji? Załóżmy, że nasza funkcja pochodzi z zewnętrznej biblioteki, która została pobrana z internetu i nie mamy możliwości dokonywania w niej zmian. Więc jak będzie wyglądało rozszerzenie działania naszej funkcji?
Cóż, można by zmodyfikować nasz kod w następujący sposób:
import datetime def print_hello_jaktestowac(): print('Hello jaktestowac!') print(datetime.datetime.now()) print_hello_jaktestowac()
Wynik działania:
2018-12-23 12:20:58.406576 Hello jaktestowac!
Jakoś to wygląda… ale co w przypadku, gdybyśmy mieli wiele użyć naszej funkcji print_hello_jaktestowac
? Albo mielibyśmy kilka różnych funkcji, dla których chcielibyśmy uzyskać identycznej rozszerzenie związane z poprzedzeniem informacją o czasie?
Moglibyśmy oczywiście zmodyfikować wywołanie każdej z tych funkcji, lub jeśli mamy możliwość modyfikacji dodać w nich potrzebny kod. Spowoduje to dużo, ciężkiego w utrzymaniu, powtórzonego kodu. Tutaj zaczyna pojawiać się problem związany z duplikacją kodu:
import datetime def print_hello_jaktestowac(): print('Hello jaktestowac!') print(datetime.datetime.now()) print_hello_jaktestowac() # more code... print(datetime.datetime.now()) print_hello_jaktestowac()
Zmiany w zduplikowanym kodzie
Jak widzisz, przed każdym wywołaniem naszej (pamiętaj, niezmienialnej) funkcji musimy wykonać dodatkowe wywołanie poprzedzającej funkcji. Co jeśli stwierdzimy, że jednak nie chcemy wyświetlać daty a np: string z tekstem Say hello to jaktestowac
? Oznacza to, że wszędzie tam gdzie użyliśmy wypisania daty będziemy musieli teraz zmienić tę linijkę na nową. Sporo roboty – wolelibyśmy to mieć w jednym miejscu. Po części moglibyśmy to załatwić osobną funkcją. Np. tak:
import datetime def print_hello_jaktestowac(): print('Hello jaktestowac!') def print_date(): print(datetime.datetime.now()) print_date() print_hello_jaktestowac() # more code... print_date() print_hello_jaktestowac()
Idealne rozwiązanie?
Jednak jakby tak można powiązać wykonanie tych funkcji razem, tak aby od razu było widać, że wykonanie ich jedna po drugiej stanowi jakąś powiązaną całość.
Można je zamknąć w funkcję?
def print_date_and_hello(): print_date() print_hello_jaktestowac()
Czyli nie zmodyfikowaliśmy ciała funkcji print_hello_jaktestowac
i dodatkowo możemy obie funkcje na raz wykonać za pomocą jednego polecenia. To świetne rozwiązanie gdy chcemy używać print_date()
tylko razem z print_hello_jaktestowac
. Gdy będziemy chcieli dodać nasz print_date()
przed inną funkcją wtedy będziemy musieli napisać kompletnie nową funkcję, np:
def print_bye_jaktestowac(): print('Bye jaktestowac!') def print_date_and_bye(): print_date() print_bye_jaktestowac()
Całość razem wyglądała by tak:
import datetime def print_hello_jaktestowac(): print('Hello jaktestowac!') def print_date(): print(datetime.datetime.now()) def print_date_and_hello(): print_date() print_hello_jaktestowac() def print_bye_jaktestowac(): print('Bye jaktestowac!') def print_date_and_bye(): print_date() print_bye_jaktestowac() print_date_and_hello() # more code... print_date_and_bye()
W obu przypadkach (dla print_hello_jaktestowac
i print_bye_jaktestowac
) wykonujemy bardzo podobne rzeczy – chcemy, aby dana linia kodu wykonała się przed jakąś funkcją. Do stworzenia takiego mechanizmu, który pozwoli nam dodać identyczny kod do różnych funkcji, służy właśnie proces dekorowania. Dla uproszczenia wracamy do poprzedniej wersji naszego kodu:
import datetime def print_hello_jaktestowac(): print('Hello jaktestowac!') print(datetime.datetime.now()) print_hello_jaktestowac() # more code... print(datetime.datetime.now()) print_hello_jaktestowac()