
Gatling. Refaktoryzacja / reorganizacja kodu skryptu. Część 5.
W tym artykule zajmiemy się refaktoryzacją/reorganizacją kodu skryptu, dzieląc skrypt na mniejsze części. Pozwoli to na utworzenie dwóch różnych scenariuszy testowych dla różnych użytkowników. Wprowadzimy zmiany w postaci dalszej parametryzacji skryptu m.in. wstawiając dane do formularza logowania odczytane z plików. Odczytamy także dane przekazane w formacie JSON i użyjemy ich w skrypcie. Dodatkowo aby ułatwić późniejsze zarządzenie kodem utworzymy w skrypcie dodatkowe zmienne.
Od czasu opublikowania ostatniego artykułu z tej serii została wydana nowa wersja narzędzia – Gatling 3.0. Najnowszą stabilną wersją jest Gatling 3.3.1, udostępniona w listopadzie 2019 roku na której uruchomimy utworzone skrypty. W niniejszym artykule użyjemy tej właśnie wersji do uruchamiania modyfikowanego skryptu. Skrypt nadal przygotowujemy pod testy aplikacji TestArena w wersji 3.1.1085. Będąc w posiadaniu skryptu nagranego na wersji 2.3 Gatlinga lub wcześniejszej konieczna jest jego modyfikacja zgodnie ze wskazówkami z przewodnika po migracji. W skrypcie przygotowanym w poprzednim artykule „Gatling. Uruchomienie nagranego skryptu. Część 4.„ w ramach migracji zmieniamy nazwę metody określającej jeden z podstawowych parametrów konfiguracyjnej protokołu HTTP, tj. zmieniamy wielkość liter w zapisie z .baseURL(„…”) na .baseUrl(„…”). Taka zmiana w naszym skrypcie wystarczy do dokonania migracji z wersji 2.3 na 3.0.
Wyodrębnienie procesów (grupowanie akcji)
W tym momencie posiadamy gotowy i działający skrypt. Skrypt ma postać i jednego zwartego scenariusza, w którym możemy wyodrębnić kilka części odpowiadających konkretnym funkcjonalnościom lub też procesom zachodzącym w aplikacji, są to:
- logowanie użytkownika do aplikacji,
- przejście do listy zadań,
- dodawanie nowego zadania,
- wylogowanie użytkownika.
Wyodrębnienie takich fragmentów pozwala w przejrzysty sposób definiować scenariusze oraz ułatwia ich zmianę zależnie od potrzeb, umożliwi także uruchomienie w jednym skrypcie kilku scenariuszy. Przed wprowadzeniem pierwszych zmian tworzymy kopię skryptu oraz zmieniamy nazwę pliku i scenariusza na „RecordedSimulation03”. W pliku kopiujemy fragmenty kodu odpowiadające danym funkcjonalnościom do nowych elementów zwanych obiektami singleton. W języku scala do definicji tych elementów służy słowo kluczowe ‘object’, dlatego utworzymy kolejno elementy:
- object LogIn,
- object TaskList,
- object AddTask,
- object LogOut.
W każdym z tych obiektów tworzymy zmienną z określoną nazwą, np. dla ‘object LogIn’ jest to zmienna ‘login’. Do każdej z takich zmiennych przypisujemy odpowiedni fragment kodu scenariusza.
Definicje headerów pozostawiamy przed definicjami obiektów, aby uniknąć ich dublowania. W kodzie scenariusza widzimy, że żądania ‘request_19’ i ‘request_21’ pokrywają się z żądaniami ‘request_4‘ i ‘request_6’. Daje to nam możliwość utworzenie jednego obiektu ‘TaskList’, który będzie wywoływany dwukrotnie w różnych miejscach scenariusza. Wraz z wprowadzonymi zmianami w kodzie skryptu zmieni się forma definicji scenariusza. Wcześniej od razu przy przypisaniu scenariusza do zmiennej ‘scn’ oraz zdefiniowaniu nazwy ‘scenario(„RecordedSimulation03”)’ zamieszczony był cały ciąg instrukcji przedstawiający operacje, jakie wirtualny użytkownik ma kolejno do wykonania. Po rozdzieleniu i pogrupowaniu instrukcji i przypisaniu ich do zmiennych w obiektach zmieniamy definicję scenariusza. W definicji scenariusza w metodzie ‘exec’ umieszczamy zmienne obiektów, które zdefiniowaliśmy wcześniej tj. LogIn.login, TaskList.tasklist, AddTask.addtask, TaskList.tasklist, LogOut.logout. Po tych zmianach zmieniamy zapis definicji scenariusza z:
val scn = scenario("RecordedSimulation03")
na:
val scn = scenario("RecordedSimulation03").exec(LogIn.login, TaskList.tasklist, AddTask.addtask, TaskList.tasklist, LogOut.logout)
Kodu skryptu po pierwszych zmianach jest zaprezentowany na listing 1, gdzie tak jak w poprzednich artykułach dane prywatne takie, jak adresy e-mail oraz hasła zostały zamienione ciągiem znaków ‘x’.
package testarena import scala.concurrent.duration._ import io.gatling.core.Predef._ import io.gatling.http.Predef._ import io.gatling.jdbc.Predef._ import java.time.LocalDate import java.time.format.DateTimeFormatter class RecordedSimulation03 extends Simulation { val headers_0 = Map( "Accept" -> "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Upgrade-Insecure-Requests" -> "1") val headers_6 = Map( "Content-Type" -> "application/x-www-form-urlencoded; charset=UTF-8", "X-Requested-With" -> "XMLHttpRequest") val headers_9 = Map( "Accept" -> "application/json, text/javascript, */*; q=0.01", "X-Requested-With" -> "XMLHttpRequest") val headers_18 = Map("X-Requested-With" -> "XMLHttpRequest") object LogIn { // Otwarcie strony val login = exec(http("request_0") .get("/") .headers(headers_0) .check(regex("""<input type="hidden" name="csrf" value="([a-z0-9]+)" id="csrf">""").saveAs("csrf")) //zapis wartości do zmiennej ) .pause(6) // Logowanie .exec(http("request_1") .post("/logowanie") .headers(headers_0) .formParam("email", "xxxxxxxx@xxxxxxxx") .formParam("password", "xxxxxxxx") .formParam("login", "Zaloguj") .formParam("remember", "0") .formParam("csrf", "${csrf}") //użycie zapisanej zmiennej ) .pause(4) } object TaskList { // Zadania val tasklist = exec(http("request_4") .get("/PP1/tasks") .headers(headers_0)) .pause(244 milliseconds) .exec(http("request_6") .post("/multi_select_load_ajax") .headers(headers_6) .formParam("name", "task167")) .pause(10) } object AddTask { val tomorrow = LocalDate.now.plusDays(1) val tomorrowf = tomorrow.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")) // Dodanie zadania val addtask = exec(http("request_7") .get("/PP1/task_add") .headers(headers_0) .check(regex("""<input type="hidden" name="csrf" value="([a-z0-9]+)" id="csrf">""").saveAs("csrf")) //zapis wartości do zmiennej ) .pause(15) // Wypełnianie formularza .exec(http("request_9") .get("/PP1/environment_list_ajax?q=S") .headers(headers_9) ) .pause(3) .exec(http("request_10") .get("/PP1/version_list_ajax?q=") .headers(headers_9) ) .pause(228 milliseconds) .exec(http("request_11") .get("/PP1/version_list_ajax?q=W") .headers(headers_9) ) .pause(10) .exec(http("request_12") .post("/PP1/project_user_list_ajax") .headers(headers_6) .formParam("q", "xxxxxxxxx@xxxxxxxx")) .pause(10) // Zapis formularza .exec(http("request_13") .post("/PP1/task_add_process") .headers(headers_0) .formParam("backUrl", "http://testarena.com/PP1/tasks") .formParam("title", "Zadanie z003") .formParam("description", "Opis zadania z003") .formParam("releaseName", "Wydanie 01") .formParam("releaseId", "132") .formParam("environments", "75") .formParam("versions", "98") .formParam("priority", "2") .formParam("dueDate", s"${tomorrowf} 23:59") .formParam("assigneeName", "Tester Testowy (xxxxxxxxx@xxxxxxxx)") .formParam("assigneeId", "3") .formParam("tags", "") .formParam("save", "Zapisz") .formParam("csrf", "${csrf}") //użycie zapisanej zmiennej ) .pause(101 milliseconds) .exec(http("request_18") .post("/PP1/comment_list_by_task_ajax/357") .headers(headers_18) ) .pause(10) } object LogOut { // Wylogowanie val logout = exec(http("request_22") .get("/wyloguj") .headers(headers_0) ) } val httpProtocol = http .baseUrl("http://testarena.com") .acceptHeader("*/*") .acceptEncodingHeader("gzip, deflate") .acceptLanguageHeader("pl,en-US;q=0.7,en;q=0.3") .userAgentHeader("Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:56.0) Gecko/20100101 Firefox/56.0") val scn = scenario("RecordedSimulation03") .exec(LogIn.login, TaskList.tasklist, AddTask.addtask, TaskList.tasklist, LogOut.logout) setUp(scn.inject(atOnceUsers(1))).protocols(httpProtocol) }
Listing 1. Kod skrypt po wyodrębnieniu procesów biznesowych/funkcjonalności
Parametryzacja zmiennych (formularzy)
W poprzednim artykule wykonaliśmy parametryzację zmiennych dynamicznych (parametr ‘csrf’) z formularzy aplikacji co było konieczne do wykonania skryptu. W tym punkcie wykonamy parametryzację danych, które są wykorzystywane w scenariuszu przez wirtualnego użytkownika. Zastosujemy metodę wprowadzania danych do skryptu ze źródeł takich jak pliki zewnętrzne. Pobierzemy dane z odpowiedzi na żądanie, która jest w formacie JSON. Odczytamy też kilka danych z kodu html strony, które zapiszemy do zmiennych, a następnie użyjemy w parametrach wysyłanych przez formularze.
Wczytanie danych z pliku
DSL Gatlinga umożliwia wprowadzanie danych do scenariusza z różnych źródeł. Do wprowadzania danych służy komponent zwany feederem.
Feeder jest źródłem danych, które mapuje zmienne z wartościami i przez metodę ‘feed’ dostarcza dane do sesji użytkownika, przez co każdy z wirtualnych użytkowników korzysta z tego samego feedera. Każde wywołanie metody feed powoduje odczytanie jednego rekordu z feedera, a dane wstawione są do sesji użytkownika wirtualnego. Dostępnych jest kilka różnych typów feederów. Oprócz feedera ‘csv’, który odczytuje dane z plików w formacie csv, można także wczytać dane z plików, w których wartości są rozdzielone tabulatorem czy też średnikiem używając odpowiednio feedera ‘tsv’ lub ‘ssv’. Dostępne również są feedery odczytujące dane z formatem JSON, tj. z pliku feederem ‘jsonFile’ lub spod adresu url feeder ‘jsonUrl’. Jest także możliwość wczytania danych z baz danych poprzez połączenie JDBC feederem ‘jdbcFeeder’, czy też ze zbiorów danych Redis za pomocą ‘redisFeeder’.
Do naszego skryptu wstawimy feedera ‘csv’, który z plików csv będzie wczytywać dane do logowania dla użytkownika z rolą lidera oraz dla użytkownika z rolą testera. Zastosowanie dwóch plików powinno ułatwić użycie dwóch różnych scenariuszy z danymi do logowania dla różnych użytkowników – osobnego dla użytkownika lidera oraz testera.
Domyślnie pliki, których dane mają zasilać feedery, powinny być umieszczone w podkatalogu ‘user-files\resources’ w katalogu domowym Gatlinga. Ścieżkę do plików można zmienić w pliku konfiguracyjnym. W wersjach starszych niż 3.0 pliki te umieszczane były w katalogu ‘user-files\data’. W plikach csv w pierwszym wierszu wstawiamy nazwy kolumn: login,password,fullName. Nazwy te w skrypcie będą nazwami zmiennych. W drugim wierszu (i kolejnych, jeśli będzie taka potrzeba) umieścimy konkretne dane do logowania dla istniejących kont w aplikacji, dane te będą wykorzystywane przez użytkowników wirtualnych.
Zawartość utworzonych plików będzie podobna do poniższego tekstu w którym dane prywatne takie jak adresy e-mail oraz hasła zostały zamienione ciągiem znaków ‘x’:
login,password,fullName xxxxxxxx@xxxxxxxx,xxxxxxxx,Lider Testów |
Dla utworzonych plików csv w skrypcie definiujemy feedery:
// feeders val csvLiderFeeder = csv("danelidera.csv").circular // login,password,fullName val csvTesterFeeder = csv("danetestera.csv").circular // login,password,fullName
gdzie:
– ‘csvLiderFeeder‘ i ‘csvTesterFeeder‘ to nazwy definiowanych feederów,
– ‘csv(„nazwapliku.csv”)‘ określa feedera pobierającego dane z pliku ‘nazwapliku.csv‘, w którym dane odseparowane są przecinkami (csv),
– ‘circular‘ – metoda określająca sposób (strategię) odczytu danych z pliku, circular oznacza, że po odczytaniu ostatniego wiersza z pliku, feeder wraca na początek.
Innymi metodami odczytu są ‘queue‘, ‘random‘, ‘shuffle‘. W ‘queue‘ dane są pobierane kolejno jak w kolejce, ale jeśli kolejka nie jest wystarczająco długa, wystąpi błąd. W ‘shuffle‘ dane najpierw są przetasowane, następnie pobierane zgodnie z metodą ‘queue‘. W ‘random‘ dane z feedera są pobierane w sposób losowy, ale w odróżnieniu od metody ‘shuffle’ pobrane dane mogą się powtarzać.
Poza plikami csv do feedera można dostarczyć pliki, w których separatorem jest tabulator – ‘tsv(„nazwapliku.tsv”)‘, średnik – ‘ssv(„nazwapliku.ssv”)‘. Można też użyć własnego separatora danych np. ‘#’ stosując feedera ‘separatedValues(„nazwapliku.txt”, ‚#’)‘.
Chcąc, aby np. użytkownicy wykonywali różne scenariusze, to obok definicji obecnego scenariusza dodamy definicję kolejnego. W naszym skrypcie będą to dwa scenariusze dla użytkowników z różnymi rolami, tj. scenariusz Lider dla użytkowników z rolą lidera w projekcie i danymi pobieranymi z pliku „danelidera.csv”, oraz scenariusz Tester dla użytkowników z rolą testera i danymi pobieranymi z pliku „danetestera.csv”.
Definicje scenariuszy dla lidera i testera przedstawia poniższy fragment kodu skryptu.
val scnLider = scenario("Lider") .feed(csvLiderFeeder) .exec(LogIn.login, TaskList.tasklist, AddTask.addtask, TaskList.tasklist, LogOut.logout) val scnTester = scenario("Tester") .feed(csvTesterFeeder) .exec(LogIn.login, TaskList.tasklist, LogOut.logout)
Po zdefiniowaniu feederów wstawianie wartości do scenariusza odbywa się poprzez metodę ‘feed’. Każde wywołanie przez wirtualnego użytkownika metody ‘feed’ powoduje pobranie jednego rekordu z feedera zależnie od ustawionej metody odczytu. Każdy wirtualny użytkownik ma w swojej sesji do “dyspozycji” własne atrybuty o nazwie zgodnej z określonymi w wierszu nagłówkowym pliku CSV – ‘login’, ‘password’, ‘fullName’.
W kodzie skryptu scenariusza użycie danych z feedera odbywa się w taki sam sposób jak dla zmiennych dynamicznych formularzy, czyli z użyciem znaku ‘$’ oraz nawiasów klamrowych – ‘${nazwaKolumny}’. Wstawienie danych z kolumny ‚login’ i ‚password’ z pliku csv do formularza logowania przedstawia poniższy fragment kodu obsługujący logowanie.
// Logowanie .exec(http("request_1") .post("/logowanie") .headers(headers_0) // Dane do logowania z feeder (login i password) .formParam("email", "${login}") //użycie wartości z kolumny login .formParam("password", "${password}") //użycie wartości z kolumny password .formParam("login", "Zaloguj") .formParam("remember", "0") .formParam("csrf", "${csrf}") //użycie zapisanej zmiennej )
Wartość z feedera z kolumny ’fullName’ wykorzystamy w formularzu dodawania zadania w ‘request_13‘. Nie możemy jednak bezpośrednio wstawić tej wartość w parametr ‘assigneeName’ formularza, gdyż oprócz tej wartości formularz przyjmuje także odpowiedni identyfikator ‘assigneeId’. Obecnie do parametru ‘assigneeId’ formularza wstawiana jest wartość, z jaką skrypt został nagrany. Wartości w parametrach ‘assigneeName’ i ‘assigneeId’ formularza powinny się odnosić do tego samego użytkownika. Problem ten rozwiążemy pobierając odpowiednie dane z JSON co zostanie opisane w kolejnym punkcie.
Zdefiniowanie drugiego scenariusza pozwala na uruchomienie symulacji odu scenariuszy jednocześnie. Wymaga to prostej zmiany definicji symulacji. Wcześniej był uruchamiany scenariusz ‘scn’, a teraz mamy zdefiniowane dwa scenariusza – ‘scnLider’ i ‘scnTester’, dlatego w metodzie ‘setUp’ po przecinku dodajemy konfigurację dla drugiego scenariusza. Na obecnym etapie dla obu scenariuszy określamy takie samo obciążenie, czyli po jednym wirtualnym użytkowniku na każdy ze scenariuszy.
Nowy zapis definicji symulacji przedstawia poniższy fragment.
setUp( scnLider.inject(atOnceUsers(1)), // określenie obciążenia dla scenariusza scnLider scnTester.inject(atOnceUsers(1)) // określenie obciążenia dla scenariusza scnTester ).protocols(httpProtocol)
Po wykonaniu takiej symulacji na wygenerowanym raporcie można będzie sprawdzić liczbę aktywnych użytkowników w czasie jej wykonywania. Symulacja rozpoczyna się z dwoma aktywnymi użytkownikami, gdy krótszy scenariusz ‚scnTester’ się kończy, dalej pozostaje tylko jeden aktywny użytkownik scenariusza ‘scnLider’. Jednak definiowaniem obciążenia zajmiemy się w jednym z kolejnych artykułów.
Przekazywanie danych odczytanych z odpowiedzi
– Odpowiedz w formacie JSON
W aplikacji TestArena na formularzu dodawania zadania niektóre pola przyjmują tylko dane z listy, dane do takiej listy są przekazywane w formacie JSON. Po wybraniu pozycji z listy w polu prezentowana jest nazwa opcji, a przy zapisie formularza odpowiednie identyfikatory są przekazywane w parametrach żądania. DSL Gatlinga umożliwia w prosty sposób odczytanie danych przekazanych za pomocą JSON. Tak jak w przypadku wyciągania z odpowiedzi serwera wartości parametru ‘csrf’ użyjemy metody ‘check’ do sprawdzenia odpowiedzi i przechwycenia wybranego z niej elementu JSON. Aby z treści odpowiedzi zawierającej JSON wyodrębnić odpowiednie dane użyjemy elementu ’jsonPath(wyrażenie)’, w którym ‘wyrażenie’ jest typem string w postaci JSONPath.Wyodrębnioną wartość można zapisać pod określoną nazwą używając ‘saveAs(nazwa)’.
Zapis takiej wartości jest opcjonalny. Wartość wyodrębnionej zmiennej jest zapisywana do sesji użytkownika wirtualnego.
W naszym skrypcie symulacyjnym zastosujemy powyższe rozwiązanie w ‘object AddTask’, w którym jako parametry formularza w ‘request_13’ wysyłamy identyfikatory wybranego środowiska, wersji oraz osoby, do której przypisywane zostaje dodawane zadanie. Identyfikatory te obecnie w skrypcie są wartościami z jakimi skrypt został nagrany, co nie jest najlepszym rozwiązaniem patrząc pod kątem zrozumienia czy ewentualnych zmian w kodzie. Identyfikatory te pobierzemy z odpowiedzi na wcześniejszych żądania (‘request_9’,’request_11’, ‘request_12’). W żądaniach podamy odpowiednie nazwy dla środowiska, wersji oraz użytkownika odczytując ich identyfikatory przesłane w formacie JSON i zapiszemy je aby ostatecznie użyć je w parametrach formularza.
Rozpoczynając od ‘request_9’ w url podajemy część nazwy środowiska jaką jest litera “S”. W odpowiedź otrzymujemy dane przesłane w formacie JSON w postać:
[{"id":"75","name":"\u015arodowisko 1"},{"id":"76","name":"\u015arodowisko 2"},{"id":"77","name":"\u015arodowisko 3"},{"id":"240","name":"\u015arodowisko 4"},{"id":"241","name":"\u015arodowisko 5"}]
W kodzie skryptu w tym żądaniu dodajemy ‘check’ z ‘jsonPath(„$..id”)’ aby odczytać wartość zwróconą dla klucza ‘id’ i zapisujemy ją do zmiennej ‘environmentId’. Wyrażenie „$..id” wyciągnie wartość dla pierwszego elementu id z odpowiedzi JSON.
Fragment kodu z żądaniem przedstawiony jest poniżej.
.exec(http("request_9") .get("/PP1/environment_list_ajax?q=S") .headers(headers_9) .check( jsonPath("$..id").saveAs("environmentId") ) )
W taki sam sposób zmieniamy żądanie ’request_11’, aby pobrać i zapisać identyfikator wersji w zmiennej ‘versionId’, którą później wstawimy do parametru ‘versions‘ w formularzu. Fragment kodu z ’request_11’ przedstawiony jest poniżej.
.exec(http("request_11") .get("/PP1/version_list_ajax?q=W") .headers(headers_9) .check( jsonPath("$..id").saveAs("versionId") ) )
Żądanie ‘request_12’ przesyłane jest metodą POST i zwraca w JSON identyfikator oraz pełną nazwę użytkownika, któremu zadanie zostanie przypisane. Dane pobrane z JSON zapisujemy pod ‘assignedId’ i ‘assignedName’. Fragment kodu skryptu z ‘request_12’ z wprowadzonymi zmianami przedstawiony jest poniżej.
.exec(http("request_12") .post("/PP1/project_user_list_ajax") .headers(headers_0) .formParam("q", "Tester Testowy") .check( jsonPath("$..id").saveAs("assignedId"), jsonPath("$..name").saveAs("assignedName") ) )
W powyższym fragmencie kodu jako parametr ‘q’ w żądaniu przesyłana jest nazwa użytkownika. Zmienimy tę nazwę zastępując na nazwę pobraną z feedera z kolumny ‘fullName’. Jeśli chcemy, aby zadanie tworzone przez użytkownika z rolą lidera zostało przypisane do testera, to w pliku „danelidera.csv” zmieniamy wartość w kolumnie ‘fullName’ podając nazwę drugiego użytkownika, np. z “Lider Testów“ na “Tester Testowy“. Taka zmiana w tym przypadku nie spowoduje żadnego problemu, gdyż wartość z kolumny ‘fullName’ nie była dotychczas wykorzystana. Możliwości są różne np. możemy dodać kolejną kolumnę do pliku feedera z nazwą lub adresem email użytkownika, któremu chcemy przypisać tworzone zadanie. Warunkiem jest oczywiście tylko to, aby taki użytkownik miał utworzone konto w testowanej aplikacji.
Wartości zapisane w zmiennych ‘assignedId’ i ‘assignedName’ wstawiamy do parametrów w żądaniu ‘request_13’.
Pełny kod skryptu scenariusza po wprowadzonych zmianach przedstawia listing 2.
– Z nagłówka odpowiedzi #### DO SPRAWDZENIA
Po wysłaniu żądania ‘request_13’ z formularzem dodawania nowego zadania zostajemy przekierowani na okno szczegółów tego zadania. Następnie wywoływane jest żądanie ‘request_18’, które powinno pobrać komentarze dla utworzonego zadania. Obecnie w adresie (‘.post(„/PP1/comment_list_by_task_ajax/357”)’) żądania metodą post jest przekazywana stała wartość “357”, która nie jest zmieniana. Wartość tą można sparametryzować odczytując identyfikator dodanego zadania z ‘request_13’. Do tego celu raz jeszcze użyjemy elementu ‘check’ z DSL Gatlinga do sprawdzenia odpowiedzi oraz z pomocą wyrażenia regularnego z adresu po przekierowaniu odczytamy identyfikator zadania. Na koniec identyfikator zadania zapisujemy do zmiennej.
Rozpoczynamy więc od ‘request_13’, po wysłaniu którego jesteśmy przekierowani na nowy adres, który zawiera identyfikator utworzonego zadania. Część tego adresu, która nas interesuje ma postać “/PP1/task_view/357”, gdzie wartość ‘357’ jest przykładowym identyfikatorem zadania, który chcemy odczytać za pomoc wyrażenia regularnego. W ‘currentLocationRegex’ wzorzec wyrażenia regularnego zapiszemy w postaci „.+/task_view/([0-9]+)”, gdzie:
– „.” oznacza wystąpienie dowolnego znaku;
– „+” oznacza co najmniej jedno wystąpienie poprzedzającego go znaku;
– „/task_view/” to ciąg jaki musi występować w adresie;
– „([0-9]+)” jest szukanym ciągiem, który składa się z samych cyfr.
Odczytany identyfikator w postaci ciągu cyfra zapisujemy do zmiennej ‘taskId’ w sesji wirtualnego użytkownika.
Do ‘request_13’ dodajemy kilka linii kodu, które mają postać:
.check( currentLocationRegex(".+/task_view/([0-9]+)").saveAs("taskId") )
Zapisaną wartość w zmiennej ‘taskId’ wstawiamy do adresu żądania ‘request_18’ wysyłanego metodą post zastępując wartość zmienną.
.exec(http("request_18") .post("/PP1/comment_list_by_task_ajax/"+"${taskId}") .headers(headers_18) )
Pełny kod skryptu scenariusza po wprowadzonych zmianach przedstawia listing 2.
package testarena import scala.concurrent.duration._ import io.gatling.core.Predef._ import io.gatling.http.Predef._ import io.gatling.jdbc.Predef._ import java.time.LocalDate import java.time.format.DateTimeFormatter class RecordedSimulation03L2 extends Simulation { val headers_0 = Map( "Accept" -> "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Upgrade-Insecure-Requests" -> "1") val headers_6 = Map( "Content-Type" -> "application/x-www-form-urlencoded; charset=UTF-8", "X-Requested-With" -> "XMLHttpRequest") val headers_9 = Map( "Accept" -> "application/json, text/javascript, */*; q=0.01", "X-Requested-With" -> "XMLHttpRequest") val headers_18 = Map("X-Requested-With" -> "XMLHttpRequest") object LogIn { // logowanie użytkownika do aplikacji // Otwarcie strony val login = exec(http("request_0") .get("/") .headers(headers_0) .check(regex("""<input type="hidden" name="csrf" value="([a-z0-9]+)" id="csrf">""").saveAs("csrf")) //zapis wartości do zmiennej ) .pause(6) // Logowanie .exec(http("request_1") .post("/logowanie") .headers(headers_0) // Dane do logowania z feeder (login i hasło) .formParam("email", "${login}") .formParam("password", "${password}") .formParam("login", "Zaloguj") .formParam("remember", "0") .formParam("csrf", "${csrf}") //użycie zapisanej zmiennej ) .pause(4) } object TaskList { // przejście do listy zadań // Zadania val tasklist = exec(http("request_4") .get("/PP1/tasks") .headers(headers_0)) .pause(244 milliseconds) .exec(http("request_6") .post("/multi_select_load_ajax") .headers(headers_6) .formParam("name", "task167")) .pause(10) } object AddTask { // dodawanie nowego zadania val tomorrow = LocalDate.now.plusDays(1) val tomorrowf = tomorrow.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")) // Dodanie zadania val addtask = exec(http("request_7") .get("/PP1/task_add") .headers(headers_0) .check(regex("""<input type="hidden" name="csrf" value="([a-z0-9]+)" id="csrf">""").saveAs("csrf")) //zapis wartości do zmiennej ) .pause(15) // Wypełnianie formularza .exec(http("request_9") .get("/PP1/environment_list_ajax?q=S") .headers(headers_9) .check( jsonPath("$..id").saveAs("environmentId") // zapis identyfikatora środowiska do zmiennej environmentId ) ) .pause(3) .exec(http("request_10") .get("/PP1/version_list_ajax?q=") .headers(headers_9) ) .pause(228 milliseconds) .exec(http("request_11") .get("/PP1/version_list_ajax?q=W") .headers(headers_9) .check( jsonPath("$..id").saveAs("versionId") // zapis identyfikatora wersji do zmiennej versionId ) ) .pause(10) .exec(http("request_12") .post("/PP1/project_user_list_ajax") .headers(headers_0) .formParam("q", "${fullName}") .check( jsonPath("$..id").saveAs("assignedId"), // zapis identyfikatora użytkownika do zmiennej assignedId jsonPath("$..name").saveAs("assignedName") // zapis nazwy użytkownika do zmiennej assignedName ) ) .pause(10) // Zapis formularza .exec(http("request_13") .post("/PP1/task_add_process") .headers(headers_0) .formParam("backUrl", "http://testarena.com/PP1/tasks") .formParam("title", "Zadanie z003") .formParam("description", "Opis zadania z003") .formParam("releaseName", "Wydanie 01") .formParam("releaseId", "132") .formParam("environments", "${environmentId}") // id z "request_9" .formParam("versions", "${versionId}") // id z "request_11" .formParam("priority", "2") .formParam("dueDate", s"${tomorrowf} 23:59") .formParam("assigneeName", "${assignedName}") // name z "request_12" .formParam("assigneeId", "${assignedId}") // id z "request_12" .formParam("tags", "") .formParam("save", "Zapisz") .formParam("csrf", "${csrf}") //użycie zapisanej zmiennej .check( currentLocationRegex(".+/task_view/([0-9]+)").saveAs("taskId") // zapis identyfikatora utworzonego zadania do zmiennej taskId ) ) .pause(101 milliseconds) .exec(http("request_18") .post("/PP1/comment_list_by_task_ajax/"+"${taskId}") // id z "request_13" .headers(headers_18) ) .pause(10) } object LogOut { // wylogowanie użytkownika // Wylogowanie val logout = exec(http("request_22") .get("/wyloguj") .headers(headers_0) ) } val httpProtocol = http .baseUrl("http://testarena.com") .acceptHeader("*/*") .acceptEncodingHeader("gzip, deflate") .acceptLanguageHeader("pl,en-US;q=0.7,en;q=0.3") .userAgentHeader("Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:56.0) Gecko/20100101 Firefox/56.0") // feeders val csvLiderFeeder = csv("danelidera.csv").circular // login,password,fullName val csvTesterFeeder = csv("danetestera.csv").circular // login,password,fullName val scnLider = scenario("Lider") .feed(csvLiderFeeder) .exec(LogIn.login, TaskList.tasklist, AddTask.addtask, TaskList.tasklist, LogOut.logout) val scnTester = scenario("Tester") .feed(csvTesterFeeder) .exec(LogIn.login, TaskList.tasklist, LogOut.logout) setUp( scnLider.inject(atOnceUsers(1)), // określenie obciążenia dla scenariusza scnLider scnTester.inject(atOnceUsers(1)) // określenie obciążenia dla scenariusza scnTester ).protocols(httpProtocol) }
Listing 2. Kod skryptu testowego z zdefiniowanymi dwoma scenariuszami, z danymi pobranymi z plików z użyciem feederów oraz użyciem danych pobranych z JSON
Definiowanie stałych
Często zdarza się, że gdzieś w kodzie do pola formularza wstawiana jest wartość, którą przyjdzie mam zmieniać później z różnych powodu np. zmiany środowiska testowanego środowiska czy też projektu. Konieczne będzie wtedy przeszukiwanie kodu skryptu i zamiana takiej wartości w miejscach, w których występuje. Aby uprościć sobie tę czynność można w kodzie na początku scenariusza lub obiektu zdefiniować zmienne, które będą przechowywać takie wartości. Wartości przypisane do takich zmiennych nie będą zmieniane w innych miejscach kodu. Będą wartościami stałymi, a sam kompilator nie pozwoli na ich zmianę. Zmienne takie w scali definiujemy z użyciem słowa kluczowego ‘val’. W zmienianym skrypcie wprowadzimy takie zmiany dla kilku wartości opisanych poniżej.
– Nazwa środowiska i wersji
Do uzyskania odpowiednich odpowiedzi w zapytaniach ‘request_9’, ’request_11’ w adresach url przesyłane są wartości parametrów. Tymi wartościami są fragmenty nazw środowiska i wersji. W kodzie skryptu definiujemy więc dwie zmienne – ‘ENVIRONMENT’ i ‘VERSION’, aby uprościć sobie ich późniejsze zmiany. Zmiennym przypisujemy wartości, które są przekazywane w parametrach, czyli odpowiednio “S” i “W”:
val ENVIRONMENT: String = "S" val VERSION: String = "W"
Zdefiniowane stałe wstawiamy bezpośrednio do adresu żądania ‘request_9’ i ‘request_11’ gdzie ciąg z adresem poprzedzamy małą literą ‘s’. Co w języku scala oznacza sposób wstawiania zmiennych lub wyrażeń bezpośrednio w ciąg znaków, pozwala to umieścić w ciągu znaków zmienne, które zostaną zastąpione wartościami. Fragment kodu skryptu z żądaniem ‘request_9’ dla środowiska z użyciem zmiennej ‘ENVIRONMENT’ wygląda:
.exec(http("request_9") .get(s"/PP1/environment_list_ajax?q=$ENVIRONMENT") .headers(headers_9) .check( jsonPath("$..id").saveAs("environmentId") ) )
W żądaniu ‘request_11’ stosujemy samą zmianę, dodając ‘s’ oraz zmieniając wartość parametru na nazwę zmiennej ‘VERSION’.
– Główny adres aplikacji
W przypadku naszego skryptu główny adres url, pod którym działa aplikacja jest dwukrotnie użyty w skrypcie. Raz jako parametr baseUrl w konfiguracji protokołu HTTP, a drugi raz w żądaniu ‘request_13’ w parametrze ‘backUrl’. Tworzymy więc dla niego nową zmienną ‘BASEURL. Deklaracja i przypisanie wartości do tej zmiennej wygląda tak samo, jak w przypadku zmiennej dla środowiska i wersji, tj:
val BASEURL: String = "http://testarena.com"
Sposób użycie tej zmiennej także jest podobny do użycia wcześniej utworzonych zmiennych. W konfiguracji protokołu http w ‘baseUrl’ zamieniamy adres na zmienną:
.baseUrl(s"${BASEURL}")
Podobną podmianę robimy w ‘request_13’ zamieniając część wartości parametru ‘backUrl’ z nazwą zmiennej. Linia ma teraz postać:
.formParam("backUrl", s"${BASEURL}/PP1/tasks")
– Nazwa klucza projektu
Zmieniając wartości na zmienne widzimy powtarzającą się wartość ‘PP1’, która jest częścią adresów kolejnych żądań – jest to klucz projektu. Wartość tą także przypiszemy do zmiennej, aby łatwiej było wprowadzać późniejsze zmiany czy też uruchamiać skrypt na innym projekcie. Tak jak w poprzednich przypadkach tworzymy zmienną ‘PROJECTKEY’ do której przypisujemy powtarzającą się wartość:
val PROJECTKEY: String = "PP1"
Sposób użycia takiej zmiennej także nie różni się od wcześniejszych przypadków. Prezentuje to fragment kodu z ‘request_11’:
.exec(http("request_11") .get(s"/${PROJECTKEY}/version_list_ajax?q=$VERSION") .headers(headers_9) .check( jsonPath("$..id").saveAs("versionId") ) )
Jeśli chcielibyśmy zastosować zmienną przechowującą wartość różną dla każdego z wirtualnych użytkowników można użyć wartości którą zapiszemy w sesji takiego użytkownika. Dodawanie zmiennych do sesji użytkownika, ich prezentacja i użycie zostanie jednak opisane w następnym artykule.
Odczyt wartości z kodu strony
Na obecnym etapie zmian w skrypcie w żądaniu ‘request_13’ w parametrach ‘releaseName‘ i ‘releaseId’ mamy na stałe wprowadzoną nazwę wydania i jego identyfikator.
Na formularzu dodawania zadania w TestArena wartości w polu Wydanie jest wypełniana automatycznie danymi wydania, które jest oznaczone jako aktywne. Dane dotyczące wydania można odczytać i wstawić do parametrów w request_13 w kilka sposobów. Jednym ze sposobów jest odczytanie danych aktywnego wydania, które są przekazywane w treści w odpowiedzi na żądanie ‘request_7’. Drugim może być wysłanie nowego żądania, które zwróci listę dostępnych wydań w formacie JSON i odczytanie odpowiednich wartości z odpowiedzi. Zastosujemy pierwszy z wymienionych sposobów, używając do tego wyrażeń regularnych.
W odpowiedzi na żądanie ‘request_7’ przesyłane są dwa element input, jeden z nazwą aktywnego wydania, a drugi z identyfikatorem tego wydania:
<input type="text" name="releaseName" id="releaseName" value="Wydanie 01" class="autocomplete" maxlength="255">
<input type="hidden" name="releaseId" value="132" id="releaseId">
Z pierwszej linie potrzebujemy wyszukać wartość atrybutu ‚value’ czyli ‚Wydanie 01’. Wydanie w TestArenie może się składać z cyfr, liter, znaków specjalnych oraz spacji, gdzie długość takiego ciągu może być od 2 do 64 znaków. Można więc użyć wzorca ‚(.{2,64})’, w którym ‘.’ (kropka) – oznacza dowolny pojedynczy znak za wyjątkiem znaku nowej linii, a zapis ‘{2,64}’ oznacza ilość wystąpień znaku od 2 do 64.
Z drugiej linii potrzebujemy wyciągnąć identyfikator wydania, czyli wartość atrybutu ‚value’, który w tym przykładzie’ ma ‘132’. Identyfikator wydania będzie składać się tylko z cyfr zatem wzorzec ‘([0-9]+)’
Do kodu skryptu w zapytaniu ‘request_7’ dodajemy więc dwa nowe wiersze. Pierwszy wiersz odczyta z odpowiedzi nazwę wydania i zapisze ją w zmiennej o nazwie ‘releaseName’. Drugi odczyta z odpowiedzi identyfikator wydania i zapisze go zmiennej ‘releaseId’. Żądania ‘request_7’ z zastosowanymi wyrażeniami regularnymi przedstawia poniższy fragment kodu.
val addtask = exec(http("request_7") .get("/PP1/task_add") .headers(headers_0) .check( regex("""<input type="text" name="releaseName" id="releaseName" value="(.{2,64})" class="autocomplete" maxlength="255">""").saveAs("releaseName"), //zapis nazwy aktywnego wydania regex("""<input type="hidden" name="releaseId" value="([0-9]+)" id="releaseId">""").saveAs("releaseId"), //zapis identyfikatora aktywnego wydania regex("""<input type="hidden" name="csrf" value="([a-z0-9]+)" id="csrf">""").saveAs("csrf") //zapis wartości do zmiennej ) )
Wartości zapisane w zmiennych ‘releaseName’ i ‘releaseId’ wstawiamy do parametrów formularza, w taki sam sposób, jak pozostałe zapisywane wartości. Przedstawia to poniższy fragment kody.
.formParam("releaseName", "${releaseName}") .formParam("releaseId", "${releaseId}")
Cały kod skryptu po wprowadzonych zmianach przedstawia listing 3. W parametrach formularza wysyłanych w żądaniu ‘request_13‘ skryptu większość danych została sparametryzowana. Pozostały tylko parametr ‘priority‘, który ma jeszcze bezpośrednio określoną wartość. Zamianą tej wartości na liczbę losową zajmiemy się w kolejnym artykule przy okazji edycji danych zapisanych w sesji wirtualnego użytkownika.
package testarena import scala.concurrent.duration._ import io.gatling.core.Predef._ import io.gatling.http.Predef._ import io.gatling.jdbc.Predef._ import java.time.LocalDate import java.time.format.DateTimeFormatter class RecordedSimulation03L3 extends Simulation { val headers_0 = Map( "Accept" -> "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Upgrade-Insecure-Requests" -> "1") val headers_6 = Map( "Content-Type" -> "application/x-www-form-urlencoded; charset=UTF-8", "X-Requested-With" -> "XMLHttpRequest") val headers_9 = Map( "Accept" -> "application/json, text/javascript, */*; q=0.01", "X-Requested-With" -> "XMLHttpRequest") val headers_18 = Map("X-Requested-With" -> "XMLHttpRequest") //deklaracje stałych val ENVIRONMENT: String = "S" // nazwa użyta do listowania i wyboru środowiska val VERSION: String = "W" // nazwa użyta do listowania i wyboru wersji val PROJECTKEY: String = "PP1" // nazwa klucza projektu val BASEURL: String = "http://testarena.com" // adres url object LogIn { // logowanie użytkownika do aplikacji // Otwarcie strony val login = exec(http("request_0") .get("/") .headers(headers_0) .check(regex("""<input type="hidden" name="csrf" value="([a-z0-9]+)" id="csrf">""").saveAs("csrf")) //zapis wartości do zmiennej ) .pause(6) // Logowanie .exec(http("request_1") .post("/logowanie") .headers(headers_0) // Dane do logowania z feeder (login i hasło) .formParam("email", "${login}") .formParam("password", "${password}") .formParam("login", "Zaloguj") .formParam("remember", "0") .formParam("csrf", "${csrf}") //użycie zapisanej zmiennej ) .pause(4) } object TaskList { // przejście do listy zadań // Zadania val tasklist = exec(http("request_4") .get(s"/${PROJECTKEY}/tasks") .headers(headers_0)) .pause(244 milliseconds) .exec(http("request_6") .post("/multi_select_load_ajax") .headers(headers_6) .formParam("name", "task167")) .pause(10) } object AddTask { // dodawanie nowego zadania val tomorrow = LocalDate.now.plusDays(1) val tomorrowf = tomorrow.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")) // Dodanie zadania val addtask = exec(http("request_7") .get(s"/${PROJECTKEY}/task_add") .headers(headers_0) .check( regex("""<input type="text" name="releaseName" id="releaseName" value="(.{2,64})" class="autocomplete" maxlength="255">""").saveAs("releaseName"), //zapis nazwy aktywnego wydania regex("""<input type="hidden" name="releaseId" value="([0-9]+)" id="releaseId">""").saveAs("releaseId"), //zapis identyfikatora aktywnego wydania regex("""<input type="hidden" name="csrf" value="([a-z0-9]+)" id="csrf">""").saveAs("csrf") //zapis wartości do zmiennej ) ) .pause(15) // Wypełnianie formularza .exec(http("request_9") .get(s"/${PROJECTKEY}/environment_list_ajax?q=$ENVIRONMENT") .headers(headers_9) .check( jsonPath("$..id").saveAs("environmentId") // zapis identyfikatora środowiska do zmiennej environmentId ) ) .pause(3) .exec(http("request_10") .get(s"/${PROJECTKEY}/version_list_ajax?q=") .headers(headers_9) ) .pause(228 milliseconds) .exec(http("request_11") .get(s"/${PROJECTKEY}/version_list_ajax?q=$VERSION") .headers(headers_9) .check( jsonPath("$..id").saveAs("versionId") // zapis identyfikatora wersji do zmiennej versionId ) ) .pause(10) .exec(http("request_12") .post(s"/${PROJECTKEY}/project_user_list_ajax") .headers(headers_0) .formParam("q", "${fullName}") .check( jsonPath("$..id").saveAs("assignedId"), // zapis identyfikatora użytkownika do zmiennej assignedId jsonPath("$..name").saveAs("assignedName") // zapis nazwy użytkownika do zmiennej assignedName ) ) .pause(10) // Zapis formularza .exec(http("request_13") .post(s"/${PROJECTKEY}/task_add_process") .headers(headers_0) .formParam("backUrl", s"${BASEURL}/${PROJECTKEY}/tasks") // wartosc z BASEURL i PROJECTKEY wstawiona do backUrl .formParam("title", "Zadanie z003") .formParam("description", "Opis zadania z003") .formParam("releaseName", "${releaseName}") // name z "request_7" .formParam("releaseId", "${releaseId}") // id z "request_7" .formParam("environments", "${environmentId}") // id z "request_9" .formParam("versions", "${versionId}") // id z "request_11" .formParam("priority", "2") .formParam("dueDate", s"${tomorrowf} 23:59") .formParam("assigneeName", "${assignedName}") // name z "request_12" .formParam("assigneeId", "${assignedId}") // id z "request_12" .formParam("tags", "") .formParam("save", "Zapisz") .formParam("csrf", "${csrf}") //użycie zapisanej zmiennej .check( currentLocationRegex(".+/task_view/([0-9]+)").saveAs("taskId") // zapis identyfikatora utworzonego zadania do zmiennej taskId ) ) .pause(101 milliseconds) .exec(http("request_18") .post(s"/${PROJECTKEY}/comment_list_by_task_ajax/"+"${taskId}") .headers(headers_18) ) .pause(10) } object LogOut { // wylogowanie użytkownika // Wylogowanie val logout = exec(http("request_22") .get("/wyloguj") .headers(headers_0) ) } val httpProtocol = http .baseUrl(s"${BASEURL}") .acceptHeader("*/*") .acceptEncodingHeader("gzip, deflate") .acceptLanguageHeader("pl,en-US;q=0.7,en;q=0.3") .userAgentHeader("Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:56.0) Gecko/20100101 Firefox/56.0") // feeders val csvLiderFeeder = csv("danelidera.csv").circular // login,password,fullName val csvTesterFeeder = csv("danetestera.csv").circular // login,password,fullName val scnLider = scenario("Lider") .feed(csvLiderFeeder) .exec(LogIn.login, TaskList.tasklist, AddTask.addtask, TaskList.tasklist, LogOut.logout) val scnTester = scenario("Tester") .feed(csvTesterFeeder) .exec(LogIn.login, TaskList.tasklist, LogOut.logout) setUp( scnLider.inject(atOnceUsers(1)), // określenie obciążenia dla scenariusza scnLider scnTester.inject(atOnceUsers(1)) // określenie obciążenia dla scenariusza scnTester ).protocols(httpProtocol) }
Listing 3. Kod skryptu ze zdefiniowanymi zmiennymi, oraz danymi odczytanymi z kodu strony
Poprawa czytelności kodu i raportu
W kodzie skryptu obecnie prezentowane są nazwy żądań domyślnie nadane podczas nagrywania skryptu scenariusza czyli nadane przez Recordera tj. ‘request_0’, ‘request_1’ itp. Nazwy tych żądań można zmieniać tak, aby były bardziej zrozumiałe i czytelne. Żądania z tymi nazwami są prezentowane w raporcie w konsoli oraz w raporcie html’owym tworzonym przez Gatlinga. Nadanie im własnych nazw sprawi, że odczytywanie przez nas raportu, a także śledzenie błędów czy nawet wyeliminowaniu komentarzy z treści skryptu będzie prostsze. Kolejność z jaką żądania są prezentowane w raporcie jest zgodna z kolejnością jaką są wykonywane co także ułatwia śledzenie ewentualnych problemów oraz analizę raportu i skryptu. Dla porównania fragmenty raportów z uruchomienia skryptu przed i po zmianie nazw przedstawiają zrzuty ekranu 1, 2, 3 i 4.
Uruchamiając nasz skrypt symulacyjny zostaną uruchomione oba scenariusze – „Lider”oraz „Tester”. Żądania, które zostały wywołane w obu scenariuszach i ich kolejność, są widoczne w konsoli oraz raporcie. Liczba wywołań niektórych z nich jest większa niż 1, gdyż zostały wywołana w obu scenariuszach. Są to żądania z logowania, wylogowania, czy też wejścia na listę zadań. Fragment konsoli z logami z wykonania skryptu z dwoma scenariuszami przedstawia zrzut ekranu 1 i 3. Na zrzutach ekranu 1 i 2 są przedstawione fragmenty raportów z uruchomienia skryptu z nazwami żądań które zostały nadane przez Recordera. Z kolei na zrzutach ekranu 3 i 4 są przedstawione fragmenty raportów z uruchomienia skryptu ze zmienionymi nazwami nadanymi przez mnie. Jak można zauważyć w przypadku dłuższych scenariuszy z których będą korzystać różne osoby na pewno warto zmienić nazwy żądań
Zrzut ekranu 1. Fragment logów z konsoli z uruchomienia skryptu przed zmianą nazw żądań
Zrzut ekranu 2. Fragment raportu z uruchomienia skryptu przed zmianą nazw żądań
Zrzut ekranu 3. Fragment logów z konsoli z uruchomienia przygotowanego skryptu po zmianie nazw żądań
Zrzut ekranu 4. Fragment raportu z uruchomienia skryptu po zmianie nazw żądań
Więcej na temat feederów i ich możliwości można znaleźć w dokumentacji Gatlinga tutaj ( https://gatling.io/docs/current/).
Podsumowanie
W artykule kod skryptu podzieliliśmy na mniejsze części grupujące akcje związane z określonymi funkcjami na testowanej aplikacji. W formularzach zastosowaliśmy dane, które zapisaliśmy wcześniej do zmiennych. Użyliśmy danych, z różnych źródeł tj. takie, które odczytaliśmy z plików oraz pobraliśmy z odpowiedzi z formatu JSON, a także odczytane z kodu strony. Wprowadzone zmiany poprawiają czytelność, zrozumienie oraz ułatwiają zarówno utrzymanie kodu skryptu jak i dalszą jego rozbudowę w przyszłości.
W następnej części zajmiemy się wyświetleniem logów z wykonywanych akcji oraz danymi zapisanymi w sesji.