Request Object Pattern
Spis treści
Wstęp
Pisząc jakiekolwiek testy automatyczne, bardzo ważne jest, aby stosować różnego rodzaju wzorce projektowe. Stosując odpowiednie wzorce, zmniejszamy koszt utrzymania testów, zwiększamy szybkość ich pisania i ułatwiamy zarządzanie.
Ucząc się pisać testy wykorzystujące framework Selenium bardzo szybko poznajemy takie wzorce jak Page Object Pattern, Bot Style Tests czy bardziej skomplikowane takie jak Screenplay pattern.
Dla testów GUI ilość wzorców pisania testów jest dość spora. A jak wygląda kwestia pisania testów REST API?
Tutaj nie mamy jednego popularnego wzorca, takiego jak POP. Możemy jedynie stosować generyczne wzorce takiego typu jak Screenplay pattern.
Na ratunek przychodzi nam Request Object Pattern.
Request Object Pattern
Wzorzec oryginalnie stworzony przez Marcina Żerko, Łukasza Gonczarika w celu szybszego pisania testów REST API.
Wzorzec zakłada podobnie jak Page Object Pattern, że każde żądanie HTTP jest osobnym obiektem. Dlaczego żądanie, a nie endpoint czy serwis? Odpowiedź na to pytanie jest prosta.
W ramach jednego endpointu może mieć wiele różnego rodzaju żądań w zależności od metody HTTP. Oprócz tego ROP został stworzony, tak aby wpasowywać się w klasyczną strukturę dowolnego serwisu HTTP:
- Każde żądanie przynależy do jakiegoś endpointu,
- Każdy endpoint przynależy do jakiegoś serwisu,
- Na endpoint może przypadać wiele żądań.
Demo aplikacja
Do implementacji wykorzystamy serwis PetStore dostępny pod adresem https://petstore.swagger.io/, który oprócz działającego REST API udostępnia dokumentację w formacie OpenAPI.
Do implementacji wykorzystamy framework REST-Assured, AssertJ oraz TestNG.
Implementacja podstawowej abstrakcji
Implementacje zaczynamy od stworzenia klasy abstrakcyjnej BaseEndpoint:
import io.restassured.response.Response;
import java.lang.reflect.Type;
import static org.assertj.core.api.Assertions.assertThat;
@SuppressWarnings("unchecked")
public abstract class BaseEndpoint<E, M> {
protected Response response;
protected abstract Type getModelType();
public abstract E sendRequest();
protected abstract int getSuccessStatusCode();
public M getResponseModel() {
assertThatResponseIsNotNull();
return response.as(getModelType());
}
public E assertRequestSuccess() {
return assertStatusCode(getSuccessStatusCode());
}
public E assertStatusCode(int statusCode) {
assertThatResponseIsNotNull();
assertThat(response.getStatusCode()).as("Status code").isEqualTo(statusCode);
return (E) this;
}
protected void assertThatResponseIsNotNull() {
assertThat(response).as("Request response").isNotNull();
}
}
W ramach klasy BaseEndpoint:
- Umieszczamy dwa parametry generyczne:
- E -- odpowiadający za specyficzny endpoint serwisu,
- M -- odpowiadający za model zwracany przez dane żądanie HTTP,
Metody:
- getModelType() -- zwracającą typ modelu,
- sendRequest() -- wykonująca żądanie HTTP,
- getSuccessStatusCode() -- zwracającą kod statusowy dla sukcesu żądania,
- getResponseModel() -- zwracająca odpowiedź żądania w formie serializowanego modelu,
- assertRequestSuccess() -- sprawdzającą, czy żądanie zwróciło ustalony kod statusowy,
- assertStatusCode(int statusCode) -- sprawdzającą, czy żądanie zwróciło podany w parametrze kod statusowy,
- assertThatResponseIsNotNull() -- sprawdzającą, czy zwrócone żądanie nie jest nullem.
Kolejnym elementem jest interfejs RestService:
public interface RestService<T> {
T getServiceApi();
}
Interfejs składa się jedynie z jednego metody publicznej zwracającej typ serwisu.
Implementacja Endpointu
Mając gotową abstrakcję możemy przejść do implementacji przykładowego endpointu. Do implementacji wykorzystamy endpoint pet w serwisie pet.
import io.swagger.pet.store.tests.base.BaseEndpoint;
import io.swagger.pet.store.tests.base.ConfigurationBuilder;
import io.swagger.pet.store.tests.service.RestService;
import io.swagger.petstore.client.PetApi;
public abstract class PetApiEndpoint<E, M> extends BaseEndpoint<E, M> implements RestService<PetApi> {
@Override
public PetApi getServiceApi() {
return PetApi.pet(new ConfigurationBuilder().getRequestSpecBuilder());
}
}
Powstała powyżej klasa abstrakcyjna PetApiEndpoint rozszerza klasę BaseEndpoint oraz implementuję interfejs RestService . Ciało metody getServiceApi() bierzemy z wygenerowanego klienta API wykorzystującego standard OpenAPI.
Mając gotowy endpoint możemy przejść do implementacji żądania dla endpointu /pet/{petId} i metody GET. I tak mamy:
import io.swagger.pet.store.tests.client.PetApiEndpoint;
import io.swagger.petstore.model.Pet;
import org.apache.http.HttpStatus;
import java.lang.reflect.Type;
import java.util.function.Function;
public class GetPetApiRequest extends PetApiEndpoint<GetPetApiRequest, Pet> {
private String petId;
public GetPetApiRequest pet(String petId) {
this.petId = petId;
return this;
}
@Override
public GetPetApiRequest sendRequest() {
response = getServiceApi()
.getPetById()
.petIdPath(petId)
.execute(Function.identity());
return this;
}
@Override
protected int getSuccessStatusCode() {
return HttpStatus.SC_OK;
}
@Override
protected Type getModelType() {
return Pet.class;
}
}
Przeciążamy wszystkie istotne metody oraz dodajemy pole petId , które odpowiada za numer zwierzęcia. Metody getPetById(), petIdPath, execute() pochodzą z wygenerowanego klienta i nie są częścią wzorca.
Ostatecznie możemy utworzyć przykładowy test.
@Test
public void getPetTest() {
Pet pet = new GetPetApiRequest()
.pet("123")
.sendRequest()
.assertRequestSuccess()
.getResponseModel();
assertThat(pet.getId()).isEqualTo(123L);
}
Siła Request Object Pattern
Mocne strony abstrakcji, jaką oferuję ROP, ujawniają się dopiero w momencie, kiedy zaczynamy implementować wiele metod i endpointów. I tak dla przykładu, aby przetestować metodę POST dla endpointu /pet wystarczy stworzyć nową klasę PostPetApiRequest:
import io.swagger.pet.store.tests.client.PetApiEndpoint;
import io.swagger.petstore.model.Pet;
import org.apache.http.HttpStatus;
import java.lang.reflect.Type;
import java.util.function.Function;
public class PostPetApiRequest extends PetApiEndpoint<PostPetApiRequest, Pet> {
private Pet pet;
public PostPetApiRequest pet(Pet pet) {
this.pet = pet;
return this;
}
@Override
public PostPetApiRequest sendRequest() {
response = getServiceApi()
.addPet()
.body(pet)
.execute(Function.identity());
return this;
}
@Override
protected int getSuccessStatusCode() {
return HttpStatus.SC_OK;
}
@Override
protected Type getModelType() {
return Pet.class;
}
}
Wykorzystując ten sam schemat mamy kolejny test:
@Test
public void postPetTest() {
Pet petToBeAdded = getTestPet();
Pet createdPet = new PostPetApiRequest()
.pet(petToBeAdded)
.sendRequest()
.assertRequestSuccess()
.getResponseModel();
assertThat(createdPet.getId()).isEqualTo(petToBeAdded.getId());
}
private Pet getTestPet() {
return new Pet().id(TestUtils.nextId()).name("alex").status(AVAILABLE)
.category(new Category().id(TestUtils.nextId()).name("dog"))
.photoUrls(Arrays.asList("http://foo.bar.com/1"));
}
Podsumowanie
Dzięki zastosowaniu ROP i automatycznie wygenerowanego klienta jesteśmy w stanie szybko i sprawnie implementować kolejne endpointy i żądania. Request Object Pattern ułatwia też pisanie testów wielowątkowo szczególnie w takim frameworku jak REST-assured, który w swojej naturze pisany jest statycznie.
[[banner|restassured]]
Potrzebujesz więcej informacji?
Artykuł powstał na bazie kursu Automatyzacja testów z wykorzystaniem Selenium.
O Autorze
Nazywam się Mateusz Ciołek i od 2011 roku zajmuję się testowaniem oprogramowania ze specjalizacją w automatyzacji testów.
Dyskusja i komentarze
Masz pytania do tego wpisu? Może chcesz się podzielić spostrzeżeniami? Zapraszamy dyskusji na naszej grupie na Facebooku.