Gatling. Uruchomienie nagranego skryptu. Część 4.

Czwarta część z serii artykułów poświęconych Gatlingowi, przygotowanej przez serwis testywydajnosci.pl. Tym razem skupimy się na tym, jak przygotować skrypt, aby można go było uruchomić w Gatlingu. 

Użyjemy skryptu, który stworzyliśmy w poprzednim artykule. Zależnie od testowanej aplikacji czy też założeń testowych przed uruchomieniem skryptu będzie on wymagał wprowadzenia pewnych zmian. W artykule wyczyścimy skrypt z nadmiarowego kodu i wprowadzimy parametryzację dla wartości dynamicznych oraz wprowadzimy parę modyfikacji w kodzie, a na koniec uruchomimy nasz skrypt w naszym narzędziu.

 

Przegląd i aktualizacja skryptu ze zbędnego kodu

Przed uruchomieniem skryptu można wykonać czyszczenie skryptu z nadmiarowego kodu, parametryzację zmiennych, których wartości są generowane dynamicznie, sprawdzenie czy kroki scenariusza wykonują się poprawnie, wprowadzenie danych z zewnątrz, jeśli jest taka konieczność czy też potrzeba zastosowania różnego typu obciążenia. W naszym przypadku ograniczę się tylko do parametryzacji formularzy, aby możliwe było uruchomienie i pozytywne wykonanie scenariusza. Taka parametryzacja może okazać się konieczna w tych fragmentach kodu, które obsługują formularze w testowanej aplikacji. Zmiany i poprawki w skrypcie będą wprowadzane w kopii wcześniej nagranego skryptu, którą utworzę pod nazwą ‘RecordedSimulation02.scala’, kolejną czynnością będzie zmiana nazwy klasy w pliku

class RecordedSimulation02 extends Simulation {

 

oraz nazwy scenariusza

val scn = scenario("RecordedSimulation02")

 

Aktualizacja nagranych czasów w funkcjach pause()

Dobrą praktyką będzie również weryfikacja czasów jakie zostały nagrane pomiędzy kolejnymi żądaniami. W celu ich zmiany można wprowadzić poprawki w linach z ‘pause(...)’, o ile to konieczne. Szczególnie po pierwszym nagrywaniu, jeśli przeglądało się zawartość okna recordera, trzeba będzie poprawić wartości czasów w funkcjach pause. Czasy te powinny być jak najbardziej zbliżone do rzeczywistego czasu użytkownika wykonującego dane akcje. Dla potrzeb testu skrócę je, tak aby przy uruchamianiu symulacji z jednym użytkownikiem nie czekać zbyt długo na jej zakończenie. Przeglądając nagrany skrypt widać, że niektóre wartości w pauzach są przesadnie wysokie, dlatego zmienię niektóre wartości, a szczegóły można zobaczyć na listingu 1.

 

Usunięcie nadmiarowego kodu z żądaniami do adresów spoza adresu testarena.com.

W nagranym skrypcie widoczne są wpisy z deklaracjami zmiennych zawierające adresy URL, które nie odnoszą się bezpośrednio do aplikacji Testarena, nie ma potrzeby więc wysyłać żądań na te adresy i sprawdzać czasów ich odpowiedzi. Usuwamy wiersze ze skryptu lub na początku możemy je oznaczyć jako komentarz, a usunąć później.

val uri2 = "http://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"
val uri3 = "http://detectportal.firefox.com/success.txt"
val uri4 = "https://www.google.com"
val uri5 = "http://fonts.googleapis.com/css"

 

Razem z powyższymi zmiennymi usuwamy kod z żądaniami wysyłanymi na te adresy. Zmienna ‘uri2’ nie została użyta w kodzie, zmienna ‘uri3’ została użyta w ‘request_3’ oraz ‘request_23’ dlatego kod ten także usuwamy razem z wywołaniami funkcji ‘pause’ przed tymi żądaniami.

 .pause(2)
 .exec(http("request_3")
 .get(uri3 + "")
 .headers(headers_3))
 
 .pause(286 milliseconds)
 .exec(http("request_23")
 .get(uri3 + "")
 .headers(headers_3))

 

Po usunięciu requestów w kodzie pozostaną definicje headerów. W ‘request_3’ oraz ‘request_23’ jest użyty ‘headers_3’, który także usuwamy z kodu.

 val headers_3 = Map("Pragma" -> "no-cache")
 

 

Parametryzacja formularzy

Zmienne dynamiczne formularzy

Teraz przejdziemy do parametryzacji formularza logowania, do którego trzeba przekazać wartość zmiennej ‘csrf’.

.formParam("csrf", "4eb7b43335d196fb3ff740a0da91fec9")

 

Wartość zmiennej trzeba pobrać ze strony ‘/zaloguj’, do której użytkownik jest przekierowywany po wejściu na stronę ‘testarena.com’, czyli z zapytania ‘request_0’.

// Otwarcie strony
.exec(http("request_0")
.get("/")
.headers(headers_0))

 

Trzeba dodać kod, w którym zapiszemy wartość przekazywaną w ukrytym znaczniku w kodzie html formularza. W kodzie html kod ten wygląda następująco:

<input type="hidden" name="csrf" value="b151c8b9b8675286fe9408ec930b6793" id="csrf">

 

Aby odczytać wartość z ‘value’ użyję funkcji ‘check’ i wyrażenia regularnego, a odczytaną w ten sposób wartość zapiszę do zmiennej ‘csrf’. Kod w skrypcie będzie wyglądać jak poniżej:

// Otwarcie strony
.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
)

 

Zapisaną wartość trzeba przekazać jako parametr przesyłany metodą POST na adres ‘logowanie’ w ‘request_1’ używając znaku '$' i nawiasów klamrowych: ‘${nazwaParamatru}’:

// 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
)

 

W taki sam sposób zapiszemy i przekażemy parametr ‘csrf’ przy dodawaniu i zapisie nowego zadania. W żądaniu ‘request_7’ dodam kod zapisujący ukrytą wartość elementu ‘csrf’:

// Dodanie zadania
.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
)

 

Zapisaną wartość trzeba przekazać jako parametr przesyłany metodą POST na adres ‘/PP1/task_add_process’:

// Zapis formularza
.exec(http("request_13")
.post("/PP1/task_add_process")
.headers(headers_0)
.formParam("backUrl", "http://testarena.com/PP1/tasks")
.formParam("title", "Zadanie z001")
.formParam("description", "Opis zadania z001")
.formParam("releaseName", "Wydanie 01")
.formParam("releaseId", "132")
.formParam("environments", "75")
.formParam("versions", "98")
.formParam("priority", "2")
.formParam("dueDate", "2017-10-31 23:59")
.formParam("assigneeName", "Tester Testowy (xxxxxxxxx@xxxxxxxx)")
.formParam("assigneeId", "3")
.formParam("tags", "")
.formParam("save", "Zapisz")
.formParam("csrf", "${csrf}") //użycie zapisanej zmiennej
)

 

Zmienne czasu realizacji

W skrypcie nagraliśmy kroki, w których użytkownik dodawał nowe zadanie. W nagranym skrypcie jest widoczna data realizacji zadania z jaką nagraliśmy skrypt, ale w obecnym momencie jest to data przeszła. Aby uniknąć problemów z dodawaniem zadania, którego termin realizacji minął dopiszemy parę nowych linii kodu, które będą wstawiać do formularza dodawania zadania datę przyszłą np. datę jutrzejszą. Przed definicją scenariusza wstawiamy kod, który odczytuje lokalną datę i dodaje do niej jeden dzień, a następnie zmienia formatowanie tej daty na format, który będzie przekazany do formularza dodawania zadania:

val tomorrow = LocalDate.now.plusDays(1)
val tomorrowf = tomorrow.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"))

 

Aby można było użyć nowych funkcji konieczne jest zaimportowanie nowych bibliotek do skryptu:

import java.time.LocalDate
import java.time.format.DateTimeFormatter

 

W kodzie zapytania "request_13" zmieniamy wpis z datą

.formParam("dueDate", "2017-10-31 23:59")

 

na wpis ze zmienną, która zawiera datę jutrzejszą w określonym wcześniej formacie daty:

.formParam("dueDate", s"${tomorrowf} 23:59")

 

Nasz skrypt po wprowadzonych zmianach ma postać jak na poniższym listingu, gdzie tak jak w poprzednim artykule 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 RecordedSimulation02 extends Simulation {
 
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 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")
 
val tomorrow = LocalDate.now.plusDays(1)
val tomorrowf = tomorrow.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"))
 
val scn = scenario("RecordedSimulation02")
// Otwarcie strony
.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
)
.exec(http("request_2")
.get("/upload/avatars/user/mini/default.jpg?1507905764"))
.pause(4)
// Zadania
.exec(http("request_4")
.get("/PP1/tasks")
.headers(headers_0))
.pause(100 milliseconds)
.exec(http("request_5")
.get("/upload/avatars/user/mini/default.jpg?1507905804"))
.pause(244 milliseconds)
.exec(http("request_6")
.post("/multi_select_load_ajax")
.headers(headers_6)
.formParam("name", "task167"))
.pause(10)
// Dodanie zadania
.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
)
.exec(http("request_8")
.get("/upload/avatars/user/mini/default.jpg?1507905815"))
.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 z001")
.formParam("description", "Opis zadania z001")
.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_14")
.get("/upload/avatars/user/mini/default.jpg?1507905921"))
.exec(http("request_15")
.get("/img/is_busy.gif"))
.exec(http("request_16")
.get("/img/arrow_down_icon.png"))
.exec(http("request_17")
.get("/img/info_close_button.png"))
.exec(http("request_18")
.post("/PP1/comment_list_by_task_ajax/357")
.headers(headers_18)
)
.pause(10)
// Powrót do listy Zadania
.exec(http("request_19")
.get("/PP1/tasks")
.headers(headers_0))
.exec(http("request_20")
.get("/upload/avatars/user/mini/default.jpg?1507905943"))
.pause(308 milliseconds)
.exec(http("request_21")
.post("/multi_select_load_ajax")
.headers(headers_6)
.formParam("name", "task167"))
.pause(2)
// Wylogowanie
.exec(http("request_22")
.get("/wyloguj")
.headers(headers_0))
 
setUp(scn.inject(atOnceUsers(1))).protocols(httpProtocol)
}
Listing 1. Skrypt z wprowadzoną obsługą danych dynamicznych.

 

Zakładając, że w testach chcemy sprawdzić tylko wydajność serwera i pomijamy testy funkcjonalne aplikacji, niektóre zapytania wysłane można usunąć, szczególnie te wysyłane po elementy statyczne jak np. avatar oraz obrazki. Będa to: ‘request_2’, ‘request_5’, ‘request_8’, ‘request_14’, ‘request_20’, ‘request_15’, ‘request_16’ oraz ‘request_17’. Usuwamy z kodu wiersze z tymi żądaniami:

.exec(http("request_2")
 .get("/upload/avatars/user/mini/default.jpg?1507905764"))
 
 .pause(100 milliseconds)
 .exec(http("request_5")
 .get("/upload/avatars/user/mini/default.jpg?1507905804"))
 
 .exec(http("request_8")
 .get("/upload/avatars/user/mini/default.jpg?1507905815"))
 
 .exec(http("request_14")
 .get("/upload/avatars/user/mini/default.jpg?1507905921"))
 
 .exec(http("request_20")
 .get("/upload/avatars/user/mini/default.jpg?1507905943"))
 
 .exec(http("request_15")
 .get("/img/is_busy.gif"))
 .exec(http("request_16")
 .get("/img/arrow_down_icon.png"))
 .exec(http("request_17")
 .get("/img/info_close_button.png"))

 

Po takim zabiegu otrzymujemy nieco skróconą wersję skryptu:

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 RecordedSimulation02 extends Simulation {
 
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 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")
 
val tomorrow = LocalDate.now.plusDays(1)
val tomorrowf = tomorrow.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"))
 
val scn = scenario("RecordedSimulation02")
// Otwarcie strony
.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)
// Zadania
.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)
// Dodanie zadania
.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 z001")
.formParam("description", "Opis zadania z001")
.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)
// Powrót do listy Zadania
.exec(http("request_19")
.get("/PP1/tasks")
.headers(headers_0))
.pause(308 milliseconds)
.exec(http("request_21")
.post("/multi_select_load_ajax")
.headers(headers_6)
.formParam("name", "task167"))
.pause(2)
// Wylogowanie
.exec(http("request_22")
.get("/wyloguj")
.headers(headers_0))
 
setUp(scn.inject(atOnceUsers(1))).protocols(httpProtocol)
}
Listing 2. Skrypt po wprowadzeniu parametryzacji oraz usunięciu nadmiarowych żądań.

 

Uruchamianie skryptu po wprowadzonych zmianach w Gatlingu

Po wprowadzonych zmianach w skrypcie można uruchomić skrypt z symulacją. Opis uruchamiania skryptu jest w poprzednim artykule Gatling. Przygotowanie środowiska. Część 2. Uruchamiamy Gatlinga i wybieramy numer odpowiadający nagranemu skryptowi, jeśli oczywiście nie popełniliśmy błędu w kodzie i kompilacja skryptu zakończyła się bez błędów. W naszym przypadku będzie to ‘7’ i dajemy ‘Enter’. W kolejnych pytaniach pozostawiamy domyślne wartości wciskając ‘Enter’. W tym momencie symulacja jest uruchamiana, co jest potwierdzone odpowiednim komunikatem.

 

Zrzut ekranu 1. Uruchomienie przygotowanego skryptu.

 

Po wykonanym i poprawnie zakończonym teście można otworzyć raport ze wskazanego po zakończeniu symulacji pliku. W logach prezentowanych w oknie konsoli widoczne są kolejno wykonywane żądania (requesty) oraz przekierowania. Widocznych jest 18 wykonanych i zakończonych pozytywnie żądań, a w aplikacji TestArena zostało dodane nowe zadanie z danymi zgodnymi z tymi, które zostały przekazane w żądaniu w skrypcie.

 

   

Zrzut ekranu 2. Gatling - odtwarzanie symulacji ukończone.

 

Przegląd raportu

Skrypt wykonał się bez błędów, w raporcie także nie widać aby czasy odpowiedzi serwera dla pojedynczych żądań odbiegały od normy. Tak jak w poprzednim artykule raport nie zawiera pełnych danych o wydajności aplikacji, ale poglądowo można zobaczyć jakie dane i wykresy prezentuje m.in. informacje ogólne co do ilości wysłanych zapytań i czasów odpowiedzi, liczba aktywnych użytkowników w czasie symulacji, rozkład czasu odpowiedzi serwera, liczba zapytań na sekundę, liczba odpowiedzi na sekundę. Poniżej na dołączonych zdjęciach prezentowane są tylko informacje ogólne oraz statystyki, widać na nich, że czasy są zgodne z informacjami z konsoli.

 

 

Zrzut ekranu 3. Raport - Global Information.

 

Zrzut ekranu 4. Raport - Statistics.

 

Podsumowanie

W artykule tym przedstawiono jak przygotować skrypt do uruchomienia w Gatlingu. Zmieniono czasy oczekiwania pomiędzy żądaniami, usunięto nadmiarowy kod oraz wprowadzono parametryzację dla zmiennych dynamicznych. Po dokonanych zmianach w kodzie skrypt został wykonany i zakończył się pozytywnie. W następnym artykule zajmiemy się dalszą refaktoryzacją kodu.

 

Autor: Marcin Herber

 

SPRAWDŹ TAKŻE
Gatling. Nagrywanie skryptów. Część 3. 
Przygotowanie środowiska do testów wydajności w Gatlingu. Część 2. 
Gatling. Testy wydajności w innej formie. Część 1. 

 

 

Najbliższe terminy szkoleń

 

22-24 sierpnia - Warszawa

Dobry Kierownik Testów - Laboratorium


27-30 sierpnia - Kraków

Zawód Tester


29-31 sierpnia - Gdańsk

ISTQB Poziom Podstawowy (Foundation Level)

 

Partnerzy

Narzędzia testerskie