Spis treści
Przesył danych do serwera REST
Przesył danych w ramach komunikacji z serwerem
W celu wyświetlenia treści na stronie przeglądarka najczęściej przesyłka kilka żądań do serwera. Następnie odbiera dane o wielu obiektach, odpowiednio je renderuje i wyświetla dla użytkownika ostateczną zawartość strony. Taka komunikacja najczęściej odbywa się za pomocą plików JSON. Również w sytuacji, kiedy to użytkownik tworzy nowy lub aktualizuje istniejący zasób, to informacja o tym jak ten zasób powinien zostać zapisany wysyłana jest przeważnie przy pomocy plików JSON.
Jak można przesłać plik JSON przy użyciu RestAssured?
Jest kilka sposobów przesyłania plików typu JSON do serwera używając biblioteki RestAssured. Z tego artykułu dowiesz się jak prosto i efektywnie przesłać plik typu JSON wykorzystując proste klasy zwane POJO.
Załóżmy, że chcesz przesłać taki plik JSON:
{
"key1": "value1",
"key2": "value2"
}
Najprostszym sposobem będzie zdefiniowanie ciała metody (body) jako referencji do pliku Simple.json:
@Test
public void shouldBeAbleToSendJsonAsFile(){
File file = new File("src/test/resources/restapi/jsons/Simple.json");
given(getPostmanEchoJsonRequestSpec())
.body(file)
.when()
.post("/post")
.then()
.spec(getPostmanEchoResponseSpec())
.assertThat()
.body("json.key1", equalTo("value1"),
"json.key2", equalTo("value2"));
}
Zauważ, że choć rozwiązanie jest proste, to w dłuższej perspektywie może się okazać problematyczne w utrzymaniu. Gdybyś chciał definiować osobny plik dla każdego testu, to ilość tych plików będzie rosła liniowo wraz z liczbą testów. Czyli 100 testów = 100 plików. Tego raczej nie chcesz, prawda?
Dodatkowo z czasem możesz zechcieć przesyłać pliki JSON, których treść będzie tworzona dynamicznie (wartości atrybutów lub same atrybuty generowane automatycznie z kodu). W takiej sytuacji pozostawanie przy dedykowanych plikach znacząco utrudni Ci rozwój testów.
Co zrobić z tym fantem?
Użyj POJO!
POJO w Javie
Co to jest POJO?
Plain Old Java Object to nic innego jak prosty typ obiektowy, który posiada jedynie dane i metody do ich pobrania/aktualizacji, czyli tzw. gettery i setery. Sporadycznie może też posiadać metody ułatwiające utworzenie obiektów, czyli tzw. konstruktory. Głównym celem klasy POJO jest przechowywanie danych, dlatego nie powinny one zawierać jakiejkolwiek innej logiki oprócz wyżej wspomnianej (gettery i settery).
Do czego można wykorzystać POJO?
Klasa typu POJO może posłużyć do przechowywania danych w czasie, np. od czasu zamknięcia programu do czasu jego ponownego uruchomienia. Dane takie mogą przez ten czas „leżakować” jako pliki bądź zapisy w bazie danych. Aby taki dowolny obiekt klasy POJO zamienić na zapis trwały (np. plik) konieczne jest poddanie go serializacji. Odwrócenie tego procesu, czyli powrót z pliku do obiektu, nazywać będziemy deserializacją.
Serializacja i Deserializacja
Załóżmy, że mamy klasę reprezentującą prostą książkę składającą się jedynie z pól tytuł oraz autor. Aby móc zapisywać stan obiektu takiej klasy, np. w formie pliku będziemy musieli skorzystać z interfejsu Serializable:
public class Book implements Serializable {
private String title, author;
public Book(String title, String author) {
this.title = title;
this.author = author;
}
public String getTitle() {
return title;
}
public String getAuthor() {
return author;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Book book = (Book) o;
return Objects.equals(title, book.title) && Objects.equals(author, book.author);
}
@Override
public int hashCode() {
return Objects.hash(title, author);
}
}
Serializację i deserializację możemy przedstawić w następujący sposób:
Jak możesz zauważyć na powyższym wykresie – serializacja polega na zamianie obiektu Javowego na strumień bajtów, który następnie możemy zapisać jako plik, wpis do bazy danych lub też przetrzymać w pamięci RAM.
Deserializacja jest procesem odwrotnym, czyli polega na przywróceniu obiektu Javowego poprzez zamianę źródła danych, np. pliku, z powrotem w strumień bajtów i następnie w konkretny obiekt Javowy.
Dzięki takiemu rozwiązaniu istnieje możliwość zachowania ciągłości danych po zamknięciu i ponownym uruchomieniu programu typu Java.
Serializacja obiektu – przykład
public static File serializeObject(Book book, String fileName) {
try (ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream(fileName))){
objectOutputStream.writeObject(book);
objectOutputStream.flush();
return new File(fileName);
}catch (IOException e){
throw new RuntimeException("Not able to serialize object", e);
}
}
W powyższej metodzie przekazywana jest referencja do obiektu typu Book, która następnie zostaje zamieniona w strumień bajtów i zapisana jako plik o wskazanej nazwie fileName. Gdyby w czasie zapisu obiektu do pliku coś poszło nie tak (np. ze względu na niewłaściwą nazwa pliku) to program wyrzuci wyjątek z odpowiednią wiadomością.
Deserializacja obiektu – przykład
public static Book deserializeObject(String fileName) {
try(ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(fileName))){
return (Book) objectInputStream.readObject();
}catch (IOException | ClassNotFoundException e){
throw new RuntimeException("Not able to deserialize object", e);
}
}
W przypadku deserializacji konieczne jest zadeklarowanie na jaki typ danych (Klasa) chcemy zamienić wczytywany plik. W sytuacji gdyby omyłkowo doszło do próby wczytania innego obiektu (np. Biblioteki) na typ Książki, to pojawi się dodatkowy wyjątek ClassNotFoundException, który również powinniśmy obsłużyć w ramach działania tej metody.
Test serializacji i deserializacji obiektu – przykład
public class BookTest {
private final static String FILE_NAME = "book";
private final static Book LOTR_BOOK = new Book("Lord of The Rings", "J.R.R. Tolkien");
@Test
public void shouldBeAbleToSerializeBook(){
Serialization.serializeObject(LOTR_BOOK, FILE_NAME);
}
@Test(dependsOnMethods = "shouldBeAbleToSerializeBook")
public void deserializationShouldBeSuccessful(){
Book deserializedBook = Serialization.deserializeObject(FILE_NAME);
assertEquals(deserializedBook, LOTR_BOOK);
}
@AfterClass
public void deleteFile(){
File file = new File(FILE_NAME);
if (!file.delete()){
throw new RuntimeException("Was unable to delete file!");
}
}
}
Celem pierwszego testu (shouldBeAbleToSerializeBook) jest sprawdzenie, że w trakcie zamiany obiektu Książki na plik nie dojdzie do błędu operacji na plikach.
Natomiast w drugim teście weryfikujemy czy, powstały na skutek pierwszego testu plik, jesteśmy w stanie z powrotem zamienić na pełnoprawny obiekt typu Book.
W ramach dobrej praktyki na koniec testów posprzątamy po sobie, czyli usuniemy zakodowany w pliku obiekt typu Book tak, aby wyniki z jednego wykonania testów nie wpływały na ponowną ich egzekucję.
Na tym etapie już wiesz do czego służy serializacja w Javie. Czas na serializację w RestAssured.
POJO w Rest Assured
Serializacja i deserializacja plików JSON
Proces serializacji plików JSON jest podobny do procesu serializacji obiektu Javowego. Różnica polega tu przede wszystkim na tym, że zamiast interfejsu Serializable wykorzystywać będziemy zewnętrzną bibliotekę taką jak Jackson/JSON czy JAXB. Z pomocą biblioteki dochodzi do zmapowania obiektu na plik json, a następnie przesłania go wraz z żądaniem do serwera.
W przypadku deserializacji dochodzi do odwrócenia procesu, czyli konwersji pliku JSON otrzymanego od serwera z powrotem na obiekt Javowy.
Serializacja za pomocą prostego obiektu POJO
Utworzenie klasy POJO:
Wróćmy do pierwszego przykładu – prostego pliku JSON składającego się z dwóch atrybutów typu tekstowego:
{
"key1": "value1",
"key2": "value2"
}
Powyższy plik możemy zapisać w formie prostej klasy SimplePojo
public class SimplePojo {
private String key1;
private String key2;
public SimplePojo(String key1, String key2) {
this.key1 = key1;
this.key2 = key2;
}
public String getKey1() {
return key1;
}
public void setKey1(String key1) {
this.key1 = key1;
}
public String getKey2() {
return key2;
}
public void setKey2(String key2) {
this.key2 = key2;
}
}
Klasa składa się z dwóch pól typu String i towarzyszącym im geterom i seterom oraz konstruktora. Nie ma tutaj żadnej logiki – jest jedynie możliwość utworzenia obiektu i wczytania/zmiany jego atrybutów.
Przesłanie obiektu POJO do serwera
Aby przesłać obiekt POJO, zaczniemy od inicjalizacji nowego obiektu SimplePojo deklarując wartości ‘value1’ oraz ‘value2’ dla konstruktora. Następnie skorzystamy z RequestSpec i prześlemy referencję do utworzonego obiektu bezpośrednio do metody body. W ten sposób dajemy znać RestAssured aby podjął próbę automatycznego zmapowania obiektu na JSON:
@Test
public void shouldBeAbleToSendPojoAsJson() {
SimplePojo simplePojo = new SimplePojo("value1", "value2");
given(getPostmanEchoJsonRequestSpec())
.body(simplePojo)
.when()
.post("/post")
.then()
.spec(getPostmanEchoResponseSpec())
.assertThat()
.body("json.key1", equalTo(simplePojo.getKey1()),
"json.key2", equalTo(simplePojo.getKey2()));
}
Niestety, po uruchomieniu testu, okaże się, że mapowanie na JSON się nie powiedzie i otrzymamy taki oto błąd :
java.lang.IllegalArgumentException: Cannot serialize because no JSON or XML serializer found in classpath.
Dodanie serializera
Rozwiązaniem problemu będzie pobranie dependencji Jackson Databind, która umożliwi poprawne mapowanie naszego obiektu POJO na plik JSON. Aby dodać bibliotekę wystarczy, że zaktualizujemy plik pom.xml o poniższą dependencję:
com.fasterxml.jackson.core
jackson-databind
2.15.2
Po przeładowaniu projektu test powinien zakończyć się sukcesem! 🙌
Co w sytuacji, gdy mamy bardziej złożony obiekt JSON do przesłania?
Adnotacje @Lombok
Problem z plikami POJO jest taki, że dla każdego 1 atrybutu potrzebne są 2 metody: get i set.
Zakładając, że metoda ‘get’ jak i ‘set’ składa się z 3 linii kodu, możemy bardzo łatwo doprowadzić do sytuacji, w której tworzona klasa przekroczy ponad 100 linii kodu. Utrudni to jej czytelność oraz utrzymywalność. Aby zapobiec takiej sytuacji możemy skorzystać z adnotacji Lombok.
W dalszej części artykułu znajdziesz zapis klasy POJO z użyciem adnotacji @Data, który osobiście stosuję z względu na wygodę i czytelność adnotacji. Aczkolwiek nie jest to jedyne rozwiązanie jakie można zastosować.
Instalacja Lombok
Aby skonfigurować Lomboka na swoim środowisku:
1. Pobieramy najnowsza wersję project lombok do naszego projektu poprzez aktualizację pliku pom.xml:
org.projectlombok
lombok
1.18.28
provided
2. Włączamy procesowanie adnotacji w naszym IntelliJu. W tym celu najprościej jest kliknąć dwukrotnie klawisz ‘shift’ i wpisać ‘enable annotation processing’, a następnie w menu IJ zaznaczyć odpowiedni checkbox, tak jak na załączonym niżej screenshocie
3. Po wszystkim klikamy na przycisk ‘Apply’ i gotowe ✅
Utworzenie klasy POJO z adnotacjami
Korzystając z adnotacji @Data oraz @AllArgsConstructor możemy zdefiniować klasę POJO z 4 atrybutami typu String wykorzystując jedynie 4 linijki kodu!
@Data
@AllArgsConstructor
public class ComplexPojo {
private String key1, key2, key3, key4;
}
Tak utworzoną klasę możemy wykorzytać dokładnie w taki sam sposób jak w teście shouldBeAbleToSendPojoAsJson. Jedyną różnicą będzie tu tylko liczba atrybutów – SimplePojo ma 2, a tutaj są aż 4:
@Test
public void shouldBeAbleToSendAnnotatedPojoAsJson() {
ComplexPojo complexPojo = new ComplexPojo("value1", "value2", "value3", "value4");
given(getPostmanEchoJsonRequestSpec())
.body(complexPojo)
.when()
.post("/post")
.then()
.spec(getPostmanEchoResponseSpec())
.assertThat()
.body("json.key1", equalTo(complexPojo.getKey1()),
"json.key2", equalTo(complexPojo.getKey2()),
"json.key3", equalTo(complexPojo.getKey3()),
"json.key4", equalTo(complexPojo.getKey4()));
}
Jak wygodnie generować POJO na bazie złożonych odpowiedzi z serwera?
Co w sytuacji gdy atrybut JSON nie jest typem prostym?
Rozważmy przykład odpowiedzi z jednego z poprzednich artykułów:
{
"data": {
"id": 2,
"email": "janet.weaver@reqres.in",
"first_name": "Janet",
"last_name": "Weaver",
"avatar": "https://reqres.in/img/faces/2-image.jpg"
},
"support": {
"url": "https://reqres.in/#support-heading",
"text": "To keep ReqRes free, contributions towards server costs are appreciated!"
}
}
Odpowiedź składa się z dwóch węzłów data oraz support. W takiej sytuacji należy utworzyć 3 obiekty POJO:
- POJO dla samej odpowiedzi
- Będzie zawierać referencje do DataPojo oraz SupportPojo
- DataPojo
- Zawierające wszystkie atrybuty wymienione w węźle „data”
- SupportPojo
- Zawierające wszystkie atrybuty wymienione w węźle „support”
Dotarliśmy tym samym do punktu, w którym zastanawiasz się, jak tworzenie tak wielu klas i linii kodu ma być efektywnym podejściem w automatyzacji?
👇Dowiedz się jak automatycznie wygenerować wiele klas i zaoszczędzić mnóstwo czasu na samodzielnym pisaniu setek linii kodu 😉
Automatyczne generowanie POJO na bazie pliku JSON
Jedną z dostępnych możliwości, aby wygenerować automatycznie zestaw klas POJO na bazie dowolnego JSONa jest strona https://www.jsonschema2pojo.org/
Po wejściu na stronę możesz zdefiniować dowolną nazwę klasy głównej oraz nazwę pakietu, do którego chcesz umieścić wygenerowane klasy. Po prawej stronie masz duży wybór opcji z których możesz skorzystać. Na początek polecam Ci ustawienie opcji w formularzu jak poniżej:
Po zaznaczeniu opcji masz możliwość podglądu wynikowych klas (Preview) lub kliknięcie przycisku Zip w celu wygenerowania archiwum z klasami. Po pobraniu zipa wystarczy już tylko wypakować je i umieścić w odpowiednim pakiecie Twojego projektu.
Wygenerowane klasy będą zawierać dodatkową adnotację @Generated, wskazującą źródło pochodzenia klasy. Pozostawienie tej adnotacji może powodować problem z kompilacją, dlatego polecam jej usunięcie (wraz z importem).
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonPropertyOrder({
"data",
"support"
})
public class ReqResResponsePojo {
@JsonProperty("data")
private Data data;
@JsonProperty("support")
private Support support;
/**
* No args constructor for use in serialization
*
*/
public ReqResResponsePojo() {
}
/**
*
* @param data
* @param support
*/
public ReqResResponsePojo(Data data, Support support) {
super();
this.data = data;
this.support = support;
}
@JsonProperty("data")
public Data getData() {
return data;
}
@JsonProperty("data")
public void setData(Data data) {
this.data = data;
}
@JsonProperty("support")
public Support getSupport() {
return support;
}
@JsonProperty("support")
public void setSupport(Support support) {
this.support = support;
}
}
W wygenerowanej klasie możesz zobaczyć również adnotację @JsonProperty. Jest ona niezwykle pomocna w sytuacji, gdy atrybut Json nie będzie zapisany w charakterystycznym dla Javy camelCasie. Adnotacja ta pozwala pomóc przy mapowaniu atrybutu np. „first_name” na pole w klasie Pojo o nazwie firstName.
Zamiana JSONa z odpowiedzi z serwera na obiekt POJO
Spróbujmy zmapować otrzymany JSON z odpowiedzi serwera na przygotowaną wcześniej klasę POJO. W tym celu konieczne będzie rozszerzenie testu o metodę extract().as(), gdzie argumentem będzie przygotowana przez nas wcześniej klasa:
@Test
public void shouldBeAbleToMapJsonToPojo(){
ReqResResponsePojo pojo = given(getReqresInRequestSpec())
.when()
.get("/users/2")
.then()
.spec(getReqresResponseSpec())
.extract().as(ReqResResponsePojo.class);
assertThat(pojo.getData().getFirstName(), equalTo("Janet"));
}
Zauważ jak łatwe jest zdefiniowanie asercji , kiedy używasz obiektów typu POJO. Wszystko co należy zrobić to przyrównać oczekiwaną wartość do interesującego nas atrybutu Jsona przez hierarchiczne wywołania (.getData()->.getFirstName())
Podsumowanie
Z artykułu dowiedziałeś się co to jest serializacja i deserializacja w Javie i jak wygląda taki proces w RestAssured
Zobaczyłeś jak można definiować klasy POJO i jak optymalizować ilość kodu przez użycie adnotacji Lomboka
Na koniec odkryłeś złoty środek jak zaoszczędzić mnóstwo czasu na automatycznym generowaniu klas POJO na bazie pliku JSON
A także wiesz już jak zamienić odpowiedź z serwera na obiekt typu POJO
Eksperymentuj samodzielnie z klasami POJO w RestAssured i przekonaj się sam, jak przyspieszą one pisanie testów i ułatwiają Ci ich utrzymanie w dłuższym terminie.
Cały kod z dzisiejszego artykułu jak zwykle znajdziesz u mnie w repozytorium
Jeśli chcesz się podzielić swoim doświadczeniem z POJO to zostaw komentarz i jeśli tego jeszcze nie zrobiłeś to dopisz się do listy subskrybentów, aby nie ominęły Cię kolejne artykuły z serii RestAssured.
Po więcej materiałów zapraszam również na kanał youtube @tujestbug i na Facebooka
2 komentarze
Kasia · 02/15/2024 o 16:17
Hej. Bardzo ciekawy blog. Jeśli chodzi o stosowanie Lomboka zamiast klas w celu eliminacji nadmiarowego kodu to można tu polecić tez stosowanie klasy typu record, która weszła w ostatnich latach do Javy. Jest to niemutowalna klasa, która zajmuje zaledwie jedną linię kodu. Pozdrawiam.
public record ComplexPojo(String key1, String key2, String key3, String key4) {}
Piotr · 02/29/2024 o 22:39
Hej, trafna uwaga, jak najbardziej można wykorzystać klasę typu record.
Warto przy tym pamiętać, że celem klasy record jest przechowanie możliwie małej ilości danych, bo podobnie jak metoda im więcej ma argumentów tym mniej staje się czytelna.
Na ogół informacji z backendu jest całkiem sporo, nie wszystkie chcemy ujawniać (oznaczać jako public) i wówczas skorzystanie z lomboka może się wydać łatwiejsze 😉
Pozdrawiam!