Prezentacja z VI edycji Quality Excites.
Podczas swojego wystąpienia Tomasz przybliżył uczestnikom wzorzec Screenplay Pattern, który określany jest przez autorów mianem następcy Page Objects. Jednym z głównych założeń Screenplay Pattern jest projektowanie zorientowane na użytkownika, dzięki czemu testy przyjmują formę języka naturalnego, co ma wspomagać szybsze tworzenie i łatwiejsze utrzymanie przypadków testowych.
6. public void UserIsAbleToFilterTheList( )
{
Actor james = Actor.Named("James");
james.Can(BrowseTheWeb.With(ChromeBrowser));
james.AttemptsTo(
Start.WithAnEmptyToDoList( ),
AddATodoItem.Called("Buy some milk"),
FilterItems.ToShow("Active")
);
james.Should(seeThat(TheItems.Displayed( ), contains(("Buy some milk")));
}
9. Actor monica = Actor.Named("Monica"); // Monica, Moderator
Actor adam = Actor.Named("Adam"); // Adam, Administrator
Adam ≠ I
10. public class Actor
{
public static Actor Named(string name) { [...] }
public Actor Can(IAbility doSomething) { [...] }
public ABILITY AbilityTo<ABILITY>( ) { [...] }
public void AttemptsTo(params IPerformable[ ] tasks) { [...] }
}
18. Scenario: Adding an item to an empty list
Given that James has an empty todo list
When he adds 'Buy some milk' to his list
Then 'Buy some milk' should be recorded in his list
public void adds_Buy_some_milk_to_his_list(string item)
{
actor.AttemptsTo(AddATodoItem.Called(item));
}
19. public class AddATodoItem : Task
{
private string thingToDo;
public static AddATodoItem Called(string thingToDo)
{
return new AddATodoItem(thingToDo);
}
public void PerformAs(Actor actor)
{
actor.AttemptsTo(Enter.TheValue(thingToDo).Into(WHAT_NEEDS_TO_BE_DONE));
}
}
20. public class Enter
{
private string theText;
public static Enter TheValue(string text)
{
return new Enter(text);
}
public EnterValue Into(By by)
{
return new EnterValue(theText, by);
}
}
21. public class EnterValue : Interaction
{
private string theText;
private By by;
public void PerformAs(Actor actor)
{
var driver = actor.AbilityTo<BrowseTheWeb>( ).Driver;
driver.FindElement(by).SendKeys(theText);
}
}
22. public void AttemptsTo(params IPerformable[ ] tasks)
{
foreach(IPerformable task in tasks)
{
task.PerformAs(this);
}
}
25. public interface IQuestion<ANSWER>
{
ANSWER AnsweredBy(Actor actor);
}
public class Selected : Question<string>
{
public static Selected Filter( ) { return new Selected( ); }
public string AnsweredBy(Actor actor) { […] }
}
26. public class Actor
{
public ANSWER AsksFor<ANSWER>(IQuestion<ANSWER> question)
{
return question.AnsweredBy(this);
}
}
string filter = actor.AsksFor(Selected.Filter( ));
29. public class TodoList
{
public static By WHAT_NEEDS_TO_BE_DONE = By.CssSelector("#new-todo");
public static By CLEAR_COMPLETED = By.CssSelector("#clear-completed");
}
30. public void UserIsAbleToFilterTheList( )
{
Actor james = Actor.Named("James");
james.Can(BrowseTheWeb.With(ChromeBrowser));
james.AttemptsTo(
Start.WithAnEmptyToDoList( ),
AddATodoItem.Called("Buy some milk"),
FilterItems.ToShow("Active")
);
james.Should(seeThat(TheItems.Displayed( ), contains(("Buy some milk")));
}
31. Beyond Page Objects: Next Generation Test Automation with
Serenity and the Screenplay Pattern
(https://www.infoq.com/articles/Beyond-Page-Objects-Test-Automation-Serenity-Screenplay)
Serenity/JS
(http://serenity-js.org/)
Serenity BDD
(https://github.com/serenity-bdd)
Zdanie opisujące obiekty występujące w frameworku i zależności zachodzące między nimi.
Poszczególne elementy omówimy szczegółowo w dalszej części prelekcji.
Page Object Model pojawił się w 2011 jako propozycja rozwiązania problemów z mało czytelnymi i niestabilnymi testami pisanymi z wykorzystaniem Selenium.
Dzięki przedstawieniu interfejsu użytkownika jako zbioru klas udostępniających metody służące do nawigowania pomiędzy elementami systemu, testy takie łatwiej pisać i utrzymywać.
Główne odpowiedzialności, które wypełnia każdy Page Object to:
dostarczenie informacji na temat lokalizowania elementów na stronie
wykonywanie akcji na tych elementach i pozyskiwanie informacji na ich temat
nawigacja pomiędzy stronami
Oprócz wymienionych odpowiedzialności, Page Objecty mogą też przechowywać instancję WebDrivera, wspomagać generowanie danych testowych itd.
Przedstawiona klasa jest bardzo prosta – reprezentuje stronę logowania z kilkoma kontrolkami, dlatego jej implementacja zamknie się w kilkunastu liniach kodu.
Problem pojawia się w bardziej złożonych przypadkach, gdy na jednej stronie musimy obsłużyć kilkanaście, lub nawet kilkadziesiąt kontrolek.
Tak duże klasy mają tendencję do powiększania się z czasem – skoro i tak robi już wszystko, to jedna metoda więcej nie zaszkodzi.
Z tego powodu stają się bardzo trudne w utrzymaniu.
Zgodnie z zasadami SOLID, klasa powinna mieć tylko jedną odpowiedzialność i powinna być zamknięta na modyfikacje. W przypadku klasycznych Page Objectów obie te zasady są łamane
W jaki sposób można więc podejść do projektowania solucji testów automatycznych zgodnie z dobrymi praktykami pisania kodu?
Jednym z rozwiązań jest Screenplay pattern, a widoczny diagram jest ilustracją zdania, które pokazywałem wcześniej.
Aktor, korzystając ze swoich umiejętności, wykonuje zadania i zadaje pytania o stan systemu, w celu osiągnięcia celów biznesowych.
Zanim przejdziemy do omawiania poszczególnych klas i interfejsów, zobaczmy jak mógłby wyglądać przykładowy test, napisany z wykorzystaniem wzorca Screenplay.
Testy opisują jak użytkownik powinien oddziaływać na system, żeby osiągnąć zadany cel.
Ważną cechą tego kodu jest jego duża czytelność – autorzy bardzo duży nacisk kładą na rezygnację z używania składni języka np. słowa kluczowego ‚new’. W związku z tym inicjalizacja obiektów będzie się odbywała w metodach statycznych klas.
Analizując powyższy przykład, napisany metodą Arrange/Act/Assert:
-tworzymy aktora o imieniu James
-dajemy aktorowi możliwość korzystania z przeglądarki internetowej
-przekazujemy mu listę zadań do wykonania
-sprawdzamy czy wykonanie akcji przyniosło oczekiwany rezultat
Tak jak widzieliśmy w przykładowym kodzie – wszystko obraca się wokół aktora, więc omówimy teraz implementację tej klasy w solucji.
Dlaczego potrzebujemy aktorów?
Nadawanie imion obiektom wykonującym testy ma zastosowanie praktyczne – dzięki temu możemy generować raporty opisujące ścieżkę, jaką aktor przeszedł w celu weryfikacji danej funkcjonalności – stąd też nazwa wzorca – Screenplay = Scenariusz.
Inną kwestią jest zmiana perspektywy, z której komponujemy testy – nie są one pisane z perspektywy pierwszej osoby, tylko w kontekście aktora, któremu możemy nadać jakieś podstawowe cechy (Adam – Administrator).
Żeby zwiększyć czytelność slajdów z klas usunięte są konstruktory i niektóre pola, więc w domyśle znajdują się one gdzieś w środku, ale nie będą obecne w prezentacji.
Tak jak wspominałem wcześniej – inicjalizacja obiektu klasy odbywa się z wykorzystaniem metody statycznej, zwracającej obiekt tej klasy – w tym przypadku z przekazanym w parametrach imieniem. Pozwala to na tworzenie aktorów bez jawnego wykorzystania konstruktora w teście, a co za tym idzie – bez użycia słowa kluczowego new.
Dwie kolejne linijki odpowiadają za nadawanie i korzystanie z umiejętności, a ostatnia to metoda do której przekazujemy listę zadań do wykonania przez aktora.
Jak widać – główną odpowiedzialnością tej klasy będzie wykonywanie zadań, ale zanim o nich – krótko omówimy sobie umiejętności.
Umiejętności pozwalają aktorowi oddziałowywać na system – bez nich aktor nie byłby w stanie wykonać żadnej akcji.
Jak może wyglądać przykładowa implementacja?
Sztandarowym przykładem – w końcu opisujemy wzorzec Screenplay w kontekście testów webowych – będzie klasa umożliwiająca aktorowi korzystanie z przeglądarki.
Ponownie – obiekt klasy inicjalizujemy metodą statyczną, a jej jedynym zadaniem będzie dostarczenie w odpowiednim momencie instacji Drivera, który wykorzystamy do kontrolowania elementów na stronie.
Przy takim podejściu do umiejętności nie muszą one być powiązane z graficznym interfejsem użytkownika – możemy dać aktorowi możliwość korzystania z systemu pliku, kontrolowania bazy danych, odbierania i wysyłania wiadomości itd.
Jednym z ciekawszych zastosowań jest implementacja klasy pełniącej rolę notatnika, w której aktorzy mogą przechowywać dowolne informacje.
Zadania i Interakcje różnią się tak naprawdę tylko swoją pozycją w hierarchii klas – zobaczmy jak wygląda implementacja tych interfesjów.
Z punktu widzenia kompilatora, oba te interfejsy są tożsame – wymagą implementacji jednej metody – PerformAs.
Metoda ta jako argument przyjmuje obiekt aktora – i to w jego kontekście będzie wykonywane konkretne zadanie, bądź Interakcja.
Skoro Zadania i Interakcje działają na dobrą sprawę tak samo, to po co je rozróżniać?
Dobrze zdefiniowane warstwy abstrakcji to jeden z kluczowych czynników w projektowaniu testów automatycznych
Odseparowanie celu testu od jego implementacji technicznej – zwiększona czytelność i utrzymywalność
Osiągamy to poprzez zastosowanie hierarchicznej analizy zadań:
Goal – co użytkownik chce osiągnąć w ujęciu biznesowym
Task – co użytkownik musi zrobić, żeby osiągnąć cel – metody opisane językiem biznesowym
Interaction – jak użytkownik będzie oddziaływał na system, żeby wypełnić zadanie
W górnej części slajdu znajduje się test BDD napisany w Gherkinie.
Celem, który stawiamy przed aktorem jest w tym przypadku tytuł scenariusza – dodanie do pustej listy zadań nowego wpisu.
Żeby zobaczyć zadania, które aktor musi wykonać, aby osiągnąć ten cel – zajrzymy do definicji kroku WHEN, gdzie przekazujemy aktorowi polecenie dodania nowego wpisu o podanej w parametrze nazwie.
Klasa AddATodoItem ponownie udostępnia metodę statyczną, dzięki której zwrócimy obiekt tej klasy z przekazaną w parametrze nazwą wpisu.
Poza tym, w związku z implementacją interfesju Task, dodana została metoda PerformAs, w której ponownie wykorzystujemy metodę aktora AttemptsTo – tym razem by przekazać mu listę interakcji do wykonania.
Skąd wziął się selektor przekazywany jako parametr metody Into? O tym za chwilę, teraz przejdźmy do implementacji klasy Enter
Klasa Enter nie jest jeszcze interakcją – jest ona pewnego rodzaju klasą budującą dla typu EnterValue, do którego przekazuje potrzebne informacje.
Tworząc obiekt klasy Enter wywołujemy metodę statyczną TheValue, która ustawi nam tekst, który chcemy wpisać w pole, a następnie – skoro mamy już obiekt tej klasy, możemy wywołać metodę Into, gdzie przekazujemy odpowiedni selektor.
Klasa EnterValue, korzystając z parametrów przekazanych przez klasę Enter wykonuje swoje zadanie na podanym elemencie.
Najpierw uzyskujemy od aktora instancję Drivera, a następnie z jego pomocą wywołujemy konkretne akcje z frameworka Selenium.
Tak wygląda implementacja metody AttemptsTo w klasie aktora.
Dla każdego Zadania przekazanego do wykonania wywołujemy metodę PerformAs. Jeżeli przypomnimy sobie jak wyglądała implementacja zadania AddATodoItem, to zauważymy że też wykorzystuje ona metodę AttemptsTo, żeby przekazać aktorowi listę Interakcji.
Tak więc – dla każdego zadania, zostanie wykonana lista Interakcji, z których jest ono zbudowane, za każdym razem przekazując dalej instancję klasy aktora, w kontekście którego wykonujemy dany test.
Implementacja pytań jest bardzo podobna do konceptu interakcji – w pytaniach jednak nie oddziałowujemy bezpośrednio na elementy i nie zmieniamy ich stanu, tylko odczytujemy z nich pewne informacje.
W celu przetestowania aplikacji będziemy potrzebowali danych różnego typu – łańcuchów znaków, numerów, wartości logicznych
W tym celu interfejs pytań implementuje metodę generyczną AnsweredBy.
W metodzie tej – podobnie jak z Interakcjami – będziemy na podstawie instancji Drivera uzyskanej od aktora wyszukiwali elementy na stronie i odpytywali o ich stan.
Ponownie wykorzystujemy metodę statyczną, żeby zwrócić instancję klasy bez konieczności używania new.
Aktor implementuje metodę generyczną, dzięki czemu możemy uzyskiwać informacje na temat elementów na stronie w naszych testach.
Ostatnim elementem potrzebnym w implementacji jest udostępnienie odpowiedniej strategii umożliwiającej lokalizowanie elementów w testowanym systemie.
Każda strona w naszej aplikacji będzie miała swoją reprezentację w postaci klasy Screen.
Statyczne pola klasy dostarczają informację na temat lokalizacji obiektów na stronie.
W tym przykładzie zaimplementowaliśmy najprostszy dostępny mechanizm – korzystamy z klasy By frameworka Selenium.
Oryginalna implementacja sugeruje użycie dodatkowej klasy, która opakowuje tą klasę i umożliwia dynamiczne tworzenie selektorów np. na podstawie podanego tekstu.
Aktor posiada umiejętności, które pozwalają mu wykonywać zadania w systemie