Wstęp

Wpis ten ma na celu ogólne przybliżenie idei wzorca Screenplay Pattern zwanego też Journey Pattern, który jest ciekawą alternatywą dla Page Object Pattern w procesie tworzenia testów automatycznych stron www. Omówimy ogólnie jego ideę oraz zobaczymy na wysokopoziomowym przykładzie jak wygląda w praktyce jego zastosowanie.

W tym artykule nawiążemy także w niewielkim stopniu do następujących zagadnień:

  • Page Object Pattern (POM)
  • Behavior-driven development (BDD)
  • pięciu podstawowych założeń programowania obiektowego – SOLID:
    1. S – Single Responsibility Principle, czyli zasada pojedynczej odpowiedzialności.
    2. O – Open/Closed Principle, czyli zasada otwarte – zamknięte.
    3. L – Liskov Substitution Principle, czyli zasada podstawiania Liskov.
    4. I – Interface Segregation Principle, czyli zasada segregacji interfejsów.
    5. D – Dependency Inversion Principle, czyli zasada odwracania zależności.

Na wstępie warto zaznaczyć, iż przykłady będą głównie bazowały na naszej autorskiej implementacji wzorca w języku Python. Mogłoby tu pojawić się pytanie:

Ale po co bawić się we własne implementacje, podczas gdy dostępne są gotowe biblioteki?

Odpowiedź jest banalnie prosta 😉 Własna implementacja pozwala dobrze zrozumieć dany wzorzec, daje większą swobodę i umożliwia eksperymenty. Dodatkowo nie znalazłem implementacji w Pythonie, więc czemu by nie zrealizować własnej?

O 3 zasadach SOLID, które Screenplay Pattern wspiera

Aby poczuć jakie wartości daje nam Screenplay Pattern krótko opiszemy 3 główne zasady SOLID, które są realizowane w ramach tego wzorca.

Single Responsibility Principle

Single Responsibility Principle – pl. Zasada Jednej Odpowiedzialności.

Klasa/funkcja/metoda/obiekt powinny mieć jedną odpowiedzialność, innymi słowy – powinny odpowiadać za jak najmniejszy fragment logiki aplikacji. W definicji pojawia się sformułowanie, że taki obiekt powinien mieć tylko jeden “powód do zmiany” (ang. “reason to change”), czyli jeśli np. dana klasa odpowiada za obliczanie i wyświetlanie danych to odpowiada za dwa procesy. Oznacza to, że mogą wystąpić dwa powody do zmiany, przez co nie spełnia Single Responsibility Principle.

Open/Closed Principle

Open/Closed Principle – pl. Zasada Otwarte-Zamknięte.

Klasa/funkcja/metoda/obiekt powinny być otwarte na rozszerzenia a zamknięte na modyfikacje. Posiłkując się poniższym rysunkiem – wiertarka spełnia Open/Closed Principle, gdyż w łatwy sposób możemy rozszerzyć jej funkcjonalność, bez potrzeby modyfikacji samego urządzenia.

Interface segregation

Interface Segregation pl. Zasada Segregacji Interfejsów.

Wiele pojedynczych interfejsów, gdzie każdy odpowiada za daną funkcjonalność, jest lepszych niż jeden interfejs odpowiedzialny za wszystko. Klient implementujący dany interfejs nie powinien być zmuszony do posiadania, implementacji ani korzystania z metod, z których nie korzysta.

Rys historyczny

Dominującym wzorcem projektowym w testach automatycznych stron www jest POM, czyli Page Object Model (zwany też Page Object Pattern). Simon Stewart w roku 2009 przedstawił koncepcję tego wzorca, która miała ułatwić pisanie testów oraz poprawić utrzymywalność kodu. Sama idea niewiele się od tamtego czasu zmieniła, aczkolwiek pojawiły się mniejsze i większe usprawniania (np. Page Factory) i modyfikacje.

Jednakże, kilka lat przed POM (niektóre źródła mówią tutaj o roku 2007) Antony Marcano zaproponował inna koncepcje – wzorzec The Screenplay Pattern, w pierwszych latach istnienia zwany Journey Pattern. Reprezentuje on odrobinę inne podejście do automatyzacji niż POM:

  • większy nacisk kładzie na zasady SOLID, takie jak Single Responsibility Principle (pl. Zasada jednej odpowiedzialności), Open/Closed Principle (pl. Zasada otwarte-zamknięte) czy Interface segregation (pl. Zasada segregacji interfejsów),
  • bazuje bardziej na kompozycji niż dziedziczeniu,
  • kładzie duży nacisk na czytelność kodu i łatwość rozszerzalności testów,
  • wprowadza nowe koncepcje bazujące na testowanej domenie.

Wzorzec ten ma też pełne wsparcie we Serenity BDD framework dla języka Java.
Z racji, że Screenplay Pattern najczęściej występuje z BDD, to w tym poście skupimy się właśnie na takim połączeniu, aczkolwiek bez większych problemów można go użyć również bez BDD.

The Screenplay Pattern – założenia i idee

Screenplay Pattern wprowadza kilka koncepcji:

  • Cel (ang. Goal),
  • Aktor (ang. Actor),
  • Zadania (ang. Tasks),
  • Interakcje albo Akcje (ang. Interactions),
  • Umiejętności (ang. Abilities),
  • Zapytania (ang. Questions).

Powyższy schemat obrazuje zależności pomiędzy poszczególnymi koncepcjami. Pokrótce omówimy każdy z elementów i zależności między nimi, a następnie zobaczymy jak wygląda to w praktyce. Posłużymy się także poniższym scenariuszem BDD do lepszego zobrazowania niektórych kwestii:

Scenario: Correct log in Lost Hat shop
   Given Bob is on Lost Hat login page
   When he logs in using his credentials
   Then he should see his name on account page

Goal

Cel będziemy wykorzystywać przy opisie kolejnych elementów wzorca i nie znajduje on się bezpośrednio na przedstawionym wcześniej schemacie. Przez cel należy rozumieć, jak się zapewne domyślacie, cel danego testu. W przypadku scenariusza BDD będzie to opis scenariusza:

Scenario: Correct log in Lost Hat shop

W przypadku klasycznego testu będzie to opis, czego test dotyczy i co jest w danym przypadku sprawdzane.

Actor

We wzorcu Screenplay Pattern, aktor reprezentuje jednostkę, która w jakiś sposób wpływa na system i wykonuje na nim operacje. W zależności od testu to może być zwykły użytkownik, użytkownik z określonymi prawami, administrator albo też inny, zewnętrzny system, który komunikuje się z naszym systemem. Przyjęło się nadawać imiona naszym aktorom. W jakim celu? Między innymi aby poprawić czytelność i ich rozróżnialność. Znacznie lepiej brzmi krok:

Bob enters credentials to admin panel

niż:

User enters credentials to admin panel

Tutaj także można pokusić się o nadawanie imion w jakiś sposób powiązanych z rolą jaką pełni dany aktor, np. aktor Adam, który jest administratorem, albo Aureliusz, który jest autorem. Nazewnictwo zależy od kontekstu, ale jest wiele sposobów na ułatwienie sobie pracy i jeśli tylko coś może przynieść korzyści, dodatkowo przy niewielkim albo praktycznie zerowym narzucie pracy, to czemu tego nie zastosować? 😉

Tasks

Zadania reprezentują wysokopoziomowe aktywności, które musi wykonać nasz aktor, aby osiągnąć cel. Patrząc na nasz scenariusz, taką wysokopoziomową aktywnością będzie np.

  • z kroku When – enter credentials albo log into account
  • z kroku Given – visit web page

Przy nazywaniu zadań, powinniśmy używać nomenklatury związanej z Problem Domain (pol. Domeny Problemu), czyli z punktu widzenia naszego klienta.
Sam Problem Domain możemy, krótko opisać jako całe środowisko w jakim będzie używany dany system. Tym samym aktorzy i zadania również powinny być tworzone w tym kontekście. Tyczy się to także kolejnych elementów.

Wracając do samego zadania, jakie zdefiniujemy, będzie składało się ono z co najmniej jednej Interakcji.

Interactions

Akcjami, albo Interakcjami, będą wszystkie niskopoziomowe czynności, które wchodzą w skład danego zadania. Aby określić akcje, musimy rozbić nasze zadanie na mniejsze części. Przykładowo, zadanie enter credentials może składać się z następujących Akcji:

  • przyciśnij dany element na stronie,
  • wprowadź następujący ciąg znaków do pola tekstowego.

Przy nazywaniu interakcji/akcji, powinniśmy używać nomenklatury związanej z Solution Domain (pol. Domeny Rozwiązań), czyli z punktu widzenia często technicznego, który rozwiązuje problem naszego klienta i umożliwia realizację zadania.

Abilities

Umiejętności są to możliwości aktora, które pozwalają mu na wykonywanie akcji. Przykładowymi umiejętnościami mogą być:

  • możliwość przeglądania stron internetowych za pomocą konkretnej przeglądarki,
  • wysyłanie emaili,
  • wykonywanie zapytań po API.

Z bardziej technicznego punktu widzenia, umiejętności są klasami, które enkapsulują specyficznego “klienta”, którym może być przeglądarka, klient API, sterownik itp.

Questions

Ostatnia, ale nie mniej ważna, to koncepcja zapytań. Są to odpytania systemu o jego stan, czyli przykładowo:

  • odczytanie tekstu wyświetlanego na stronie,
  • zapytanie o dostępność danego elementu,
  • wykonanie konkretnego zapytania (np. GET) po API.

The Screenplay Pattern w praktyce

Do tej pory było głównie o teorii, teraz przyszedł czas na odrobinę przykładów praktycznych. Rzućmy ponownie okiem na nasz scenariusz a następnie przanalizujmy jego kroki, aby wskazać w nich elementy Screenplay Pattern. Zobaczymy również fragmenty przykładowej implementacji, aby sprawdzić czy faktycznie pisanie testów jest łatwe i przyjemne.

Scenario: Correct log in Lost Hat shop
   Given Bob is on Lost Hat login page
   When he logs in using his credentials
   Then he should see his name on account page

Analiza kroku Given

Rozbijmy pierwszy krok na wcześniej wymienione koncepcje (czyli aktor, zadanie, umiejętność itp). Pierwsze co może nam się nasunąć na myśl to:

  • naszym aktorem jest Bob,
  • aktor Bob musi mieć zdolność do otwierania przeglądarki.

Mamy już określonego aktora i jego umiejętności. Jak będą wyglądały zadania? Będą to:

  • otworzenie przeglądarki,
  • przejście pod podany adres.

Gdy spojrzymy na schemat to zobaczymy, że zadania składają się z akcji (lub interakcji), dlatego teraz zastanówmy się jakie akcje wejdą w skład zadań, które wcześniej zdefiniowaliśmy. Pierwsze zadanie będzie zawierało tylko jedną akcję:

  • uruchomienie przeglądarki.

Drugie zadanie, podobnie jak pierwsze, będzie zawierało też tylko jedną akcję:

  • otwarcie danej strony.

Tutaj mogą pojawić się pierwsze wątpliwości. Dlaczego tworzymy zadania, które zawierają tylko jedną akcję? Albo dlaczego nie możemy stworzyć jednego zadania, które dotyczy obu rzeczy na raz?

Jedna akcja w zadaniu wynika z samego założenia wzorca – aktor wykonuje zadanie, które składają się z akcji. Taki podział pozytywnie wpływa na rozgraniczenie odpowiedzialności pomiędzy klasami i konceptami (czyli widzimy tutaj zachowaną Single Responsibility Principle).

Dodatkowo taka granulacja zadań i akcji, które dotyczą tylko jednej czynności, pozwoli nam reużyć je w przyszłości. Lepiej podzielić zadanie, które otwiera przeglądarkę i przechodzi pod podany adres, na dwa osobne. Dzięki temu w przyszłości będziemy mogli reużyć tego drugiego, czyli odpowiedzialnego tylko za przechodzenie pod wskazany adres.

A jak implementacja pierwszego kroku może wyglądać w kodzie? Inicjalizacja aktora, oraz nadanie mu odpowiednich zdolności mogłoby odbyć się w kroku poprzedzającym scenariusz (czyli na przykład w @before), aczkolwiek my dla uproszczenia zawrzemy wszystko w kroku pierwszym:

from behave import *
from tasks.visit_web_page import VisitWebPage
from tasks.open_browser import OpenBrowser
from abilities.browse_the_web import BrowseTheWeb


@given(u'{actor_name} is on Lost Hat main page')
def step_impl(context, actor_name):
   actor = context.stage.call_to_stage_for(actor_name)
   actor.who_can(BrowseTheWeb(context).using_chrome())
   OpenBrowser(context).perform_as(actor)
   VisitWebPage(context).with_address(context.navigator.base_url).perform_as(actor)

Analizując naszą implementację (prawie) linia po linii:

  • context – jest naszym kontekstem testów, zawiera wszystkie potrzebne obiekty i w naszej implementacji przyjęliśmy założenie, że jest potrzebny do inicjalizacji obiektów,
  • stage – jest nasza sceną i służy do wywoływania (a bardziej technicznie – do inicjalizacji) aktora albo przechowywania obiektów potrzebnych w naszych spektaklach (czyli testach),
  • context.stage.call_to_stage_for(actor_name) – tworzy obiekt aktora o danym imieniu,
  • actor.who_can(BrowseTheWeb(context).using_chrome()) – nadaje aktorowi zdolność przeglądania internetu, przy użyciu przeglądarki Chrome,
  • OpenBrowser(context).perform_as(actor) – aktor wykonuje zadanie dotyczące otwarcia przeglądarki,
  • VisitWebPage(context).with_address(context.navigator.base_url).perform_as(actor) – ponownie, wskazany aktor wykonuje zadanie dotyczące otwarcia strony internetowej z konkretnym adresem.

Analiza kroku When

Brzmi nie aż tak trudno, prawda? To teraz przeanalizujmy krok drugi, który brzmiał:

   When he logs in using his credentials

Rozbijając go na czynniki pierwsze:

  • naszym aktorem jest aktor z kroku poprzedniego (czyli Bob),
  • aktor (Bob) musi wykonać zadanie polegające na wprowadzeniu swoich danych do logowania.

Na razie nie dzieje się za dużo, ale zastanówmy się nad naszym zadaniem. Na czym będzie ono polegało? Jakie akcje wchodzą w jego skład? Będą to dwie dosyć generyczne akcje:

  • kliknięcie elementu strony,
  • wypełnienie pola tekstowego.

Natomiast podczas implementacji będziemy musieli ułożyć z nich następujący ciąg:

  • kliknięcie pola tekstowego na email,
  • wypełnienie pola tekstowego podanym adresem email,
  • kliknięcie pola tekstowego na hasło,
  • wypełnienie pola tekstowego podanym hasłem,
  • kliknięcie przycisku zaloguj.

Przykładowa implementacja kroku może wyglądać w następujący sposób:

from tasks.log_into_account import LogIntoAccount


@when(u'he logs in using his credentials')
def step_impl(context):
   actor = context.stage.the_actor_in_the_spotlight()
   LogIntoAccount(context).with_credentials(actor.get_credentials()).perform_as(actor)

Analiza kroku Then

Pozostał jeszcze ostatni krok:

   Then he should see his name on account page

Krok ten będzie zawierał pytanie zadawane przez naszego aktora. Pewnym uproszczeniem będzie podejście do pytań jak do zadań, które składają się z akcji, które zwracają jakiś rezultat. Przy czym zamiast metody perform_as, która nie zwraca żadnych wartości, użyjemy asked_by, która zwróci nam wyniki akcji. Takie podejście pozwoli odrobinę ułatwić analizę i implementację, przy jednoczesnym zachowaniu zasad SOLID.

Nasz aktor zada pytanie o wyświetlaną nazwę na stronie. Będzie ono zawierało jedną akcję polegającą na pobraniu tekstu z danego elementu. Natomiast asercja znajdzie się na najwyższym poziomie w implementacji kroku.

Przykładowa implementacja kroku może wyglądać w następujący sposób:

from questions.logged_user_name import LoggedUserName


@then(u'he should see his name on account page')
def step_impl(context):
   actor = context.stage.the_actor_in_the_spotlight()
   full_name = LoggedUserName(context).asked_by(actor)
   assert actor.get_full_name() == full_name, f"Expected: {actor.get_full_name()}, actual: {full_name}"

Implementacja kroków

Mając dobrze przygotowany framework i jasno sprecyzowane scenariusze możemy łatwo automatyzować poszczególne elementy Screenplay Pattern. Wystarczy wyodrębnić poszczególnych aktorów, zadania, akcje (ect.) i możemy przystąpić do realizacji spektaklu 🙂

Podsumowanie

Po ambitnych planach własnej implementacji w języku Python i zastosowania jej w projekcie testowym zauważyliśmy kilka niewątpliwych plusów wzorca Screenplay:

  • lepsze integracja z SOLID i separację poszczególnych odpowiedzialności,
  • całkiem przyjemne i szybkie pisanie nowych testów w przypadku posiadania podstawowego frameworka,
  • pisany kod może być odrobinę bardziej ludzki i czytelny przy użyciu fluent interface, np:
    actor.attempts_to(
               StartBrowser(...).using(actor.ability_to(BrowseTheWeb))
           )
    

Jednak pojawiło i nasunęło nam się także kilka minusów:

  • wzorzec ten jest cały czas mało popularny – znalezienie pomocy przy mniej trywialnych przypadkach może być problematyczne,
  • możliwy duży próg wejścia, głównie w przypadku własnej implementacji w mniej popularnych językach,
  • niewielka liczba oraz powolny rozwój bibliotek – wzorzec opiera się głównie o gotowe biblioteki, które mogą w pewien sposób ograniczać,
  • ogólny próg wejścia może być wyższy, szczególnie dla osób początkujących i rozpoczynających swoją przygodę z automatyzacją w porównaniu do POM.

Do rozważenia pozostają następujące aspekty:

  • długoterminową utrzymywalność i rozwijanie projektu testowego,
  • wdrażanie osób początkujących do projektu i to jak sobie radzą ze Screenplay Pattern.

Powyższe zalety i wady są czysto subiektywne, bazują na tym, co stworzyliśmy w naszym niewielkim projekcie i obserwacjach jakie poczyniliśmy w trakcie pracy ze Screenplay Pattern. Warto dodać, że głównie do tej pory działaliśmy ze wzorcem POM, co też mogło mieć wpływ na nasze odczucia i wrażenia 🙂

Reasumując, myślimy, że warto zapoznać się z Screenplay Pattern i zobaczyć jak sprawuje się w działaniu, jak pisze się nowe testy i gdzie mogą wystąpić trudności. Do tego zachęcamy do stworzenia własnego, niewielkiego projektu testowego i do zmierzenia się z jego implementacją w praktyce.

Jeśli mielibyśmy odpowiedzieć pytania, które na pewno Wam się nasunęły:

Czy Screenplay Pattern wyprze POM? Czy Screenplay Pattern jest lepszy od POM?

Odpowiadamy – nie. Nie wydaje nam się, że Screenplay Pattern wyprze wzorzec Page Object. Screenplay Pattern ma niewątpliwie wiele zalet i mocno stawia na zgodność z SOLID, aczkolwiek wszystkie te plusy nie przeważają na jego korzyść. Wzorzec Page Object jest z nami już od wielu lat i funkcjonuje w bardzo dużej liczbie projektów. Pomimo faktu, że większośc jego implementacji nie jest zgodna ze wszystkimi dobrymi praktykami programistycznymi, to z doświadczenia możemy powiedzieć, że całkiem dobrze sprawdza się w działaniu, utrzymaniu i ogólnej łatwości we wdrożeniu (m. in. przez jego popularność). Prawdopodobnie minie jeszcze wiele lat zanim Screenplay Pattern zagości w części naszych projektów…

Dalsze plany

W tym poście poruszyliśmy jedynie niewielką część całego tematu związanego ze wzorcem Screenplay Pattern.

Jeśli jednak jesteście bardziej zainteresowani szczegółami, chcielibyście poczytać o nim więcej lub zapoznać się ze sposobem implementacji własnego rozwiązania, to dajcie znać w komentarzach 🙂

W dalszej przyszłości planujemy stworzyć kurs na temat własnej implementacji wzorca Screenplay Pattern w języku Python. Jeśli chcielibyście zobaczyć jak od zera zaprojektować i wprowadzić ten wzorzec w projekcie to piszcie w komentarzach i głosujcie na niego na naszej liście tematów na przyszłe kursy, która jest dostępna na naszej tablicy trello.

Polecane linki

Przydatne linki, na których częściowo bazowany był ten wpis oraz implementacji własnego rozwiązania:

Krzysiek Kijas
Senior Software Engineer

Z testowaniem i dbaniem o jakość oprogramowania jestem już związany od prawie dekady. Dobre podwaliny techniczne i ugruntowanie wiedzy certyfikatami pozwalają mi swobodnie pływać w tym bezkresnym oceanie technologii 😉

Na co dzień zajmuje się różnego rodzaju testami, poczynając od manualnych, eksploracyjnych, aż po tworzenie frameworków i projektowanie ich architektury. Dodatkowo od wielu lat jestem zaangażowany w prowadzenie szkoleń dla QA oraz przygotowywanie warsztatów na Quality Excites. Ciągły głód wiedzy i silna potrzeba rozwoju spowodowały, iż razem z Przemkiem stworzyliśmy inicjatywę jaktestować.pl, która była naturalnym krokiem w mojej ewolucji. Dzięki niej, mogę realizować i rozwijać swoje pasje związane z mentoringiem oraz pomocy innym w zdobywaniu wiedzy.

W wolnym czasie z pełnym poświęceniem oddaje się swoim uzależnieniom - treningom siłowym, crossfitowi, literaturze i szeroko pojętej sztuce.

Jeden komentarz

  1. Pingback: Nowości na jaktestowac.pl #4 – w03/04 (12.01-25.01.2019) - Jak Testować?

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *