Описание на русском языке смотреть тут.
- intended for testing HTTP API using the
rertofit2
library; - designed to simplify the development of autotests as much as possible to the detriment of some architectural canons and is designed to qualify a strong junior in test automation;
- designed with the ability to change the current implementation;
- ready to use project with autotests is presented in the
example
module; - if you want to ask how to do something, or to understand why something isn't working the way you expect it to, use telegram group;
Client
- choosing a converter by java type, model package name,
Content-Type
header; - converters copies the response body instead of reading the
BufferedSource
; - selection of the order of actions of the interceptor separately for Request and Response;
- logging interceptor for request/response with two
LoggingEvent
, respectively; - exclude the need to add self-signed certificates in java keystore;
Request
- using
Object
type for request body in API method (dynamic converter selection); - reading request body from file;
- simplified work with @QueryMap (reflection);
- specifying the converter for the request via the API method annotation;
Response
- work with two response models at once for positive/negative tests;
- built-in softly assertions engine;
- writing the response body to a file;
- specifying the converter for the response via the API method annotation;
Other
- soft assertions with auto-close and the option to ignore NPE (
SoftlyAsserter
); - ability to add soft assertions to the model for further use in
ResponseAsserter
; - examples of using soft assertions for response (
ExampleApiClientAssertions
); - jakarta java bean validation for any models (
BeanValidationModel
); - base class with addition properties for jackson2 models (
JacksonModelAdditionalProperties
); - allure framework support;
Primitive usage example:
public static class ExampleTests {
public interface ExampleClient {
@GET("/api/example")
DualResponse<Pet, Err> get();
}
private static final ExampleClient CLIENT = buildClient();
public void test1639328754881() {
Pet expected = new Pet().name("example");
// Response contains built-in soft assertions for checking the status, headers, and body of the response.
CLIENT.get("id_1").assertResponse(respAsserter -> respAsserter
.assertHttpStatusCodeIs(200)
.assertHttpStatusMessageIs("OK")
.assertHeaders(headersAsserter -> headersAsserter
.contentTypeIs("application/json; charset=utf-8")
.assertHeaderIsPresent("X-Request-Id")
.accessControlAllowOriginIs("*"))
.assertSucBody((asserter, actual) -> {
asserter.softly(actual::assertConsistency);
asserter.softly(() -> is("Pet.name", actual.name, expected.name));
}));
}
}
All examples below assume the use of the lombok library (for shorthand).
make ex
# or
mvn package -DskipTests=true
cd ./example
mvn test -DskipTests=false -Dmaven.test.failure.ignore=true
mvn allure:serve
- all - if you are not confused by unnecessary dependencies in the project;
- jackson - working with Jackson2 data models;
- gson - working with Gson data models;
- allure - build-in steps for API calls with request/response attachments;
- bean - data models with built-in JSR 303 bean validation (jakarta bean validator);
Example:
<dependency>
<groupId>org.touchbit.retrofit.veslo</groupId>
<artifactId>jackson</artifactId>
<version>${veslo.version}</version>
<scope>compile</scope>
</dependency>
public class BaseTest {
protected static final PetApi PET_API = createJacksonClient(PetApi.class);
static {
// localisation (jakarta assertions)
Locale.setDefault(Locale.ENGLISH);
}
private static <CLIENT> CLIENT createJacksonClient(final Class<CLIENT> clientClass) {
return new Retrofit.Builder()
.client(new OkHttpClient.Builder()
// Configure this client to follow redirects
// (HTTP status 301, 302...).
.followRedirects(true)
// follow redirects (httpS -> http and http -> httpS)
// (for test environment)
.followSslRedirects(true)
// instead of adding self signed certificates to the keystore
// (for test environment)
.hostnameVerifier(TRUST_ALL_HOSTNAME)
.sslSocketFactory(TRUST_ALL_SSL_SOCKET_FACTORY, TRUST_ALL_CERTS_MANAGER)
// Interceptor with your call handling rules
// (include `follow redirects`)
.addNetworkInterceptor(new CustomCompositeInterceptor())
.build())
.baseUrl("https://petstore.swagger.io/")
// for AResponse<> (with allure steps)
.addCallAdapterFactory(new AllureCallAdapterFactory())
// for DualResponse<> (default)
// .addCallAdapterFactory(new UniversalCallAdapterFactory())
.addConverterFactory(new JacksonConverterFactory())
// .addConverterFactory(new GsonConverterFactory())
.build()
.create(clientClass);
}
}
The mechanism for choosing the necessary converter for converting requests and responses is implemented in the classes
ExtensionConverterFactory
, JacksonConverterFactory
and GsonConverterFactory
. The converter is selected in the
following sequence:
- by annotations:
@Converters
,@RequestConverter
,@ResponseConverter
; - by raw body types (
RawBody
,File
,ResourceFile
,Byte[]
,byte[]
); - by models package name (wildcard match);
- by Content-Type header (MIME);
- by primitive/reference java types (
Byte
,Character
,Double
,Float
,Integer
,Long
,Short
,String
);
You can implement your generic factory by analogy.
public class CustomConverterFactory extends ExtensionConverterFactory {
public CustomConverterFactory() {
super(LoggerFactory.getLogger(CustomConverterFactory.class));
final JacksonConverter<Object> jacksonConverter = new JacksonConverter<>();
// use JacksonConverter for application/json, text/json content type
registerMimeConverter(jacksonConverter, APP_JSON, APP_JSON_UTF8, TEXT_JSON, TEXT_JSON_UTF8);
// use JacksonConverter for Map, List java types
registerJavaTypeConverter(jacksonConverter, Map.class, List.class);
// use custom converter for custom model
registerRawConverter(new CustomConverter<>(), CustomBody.class);
// use GsonConverter for any models in the package "com.example.model.gson" (wildcard match)
registerPackageConverter(new GsonConverter(), "com.example.model.gson");
}
}
public interface PetApi {
@POST("/v2/pet")
@RequestConverter(bodyClasses = {Pet.class}, converter = JacksonConverter.class)
@ResponseConverter(bodyClasses = {Pet.class, Err.class}, converter = JacksonConverter.class)
// or array of converters
@Converters(
request = {@RequestConverter(bodyClasses = Pet.class, converter = JacksonConverter.class)},
response = {@ResponseConverter(bodyClasses = {Pet.class, Err.class}, converter = JacksonConverter.class)}
)
AResponse<Pet, Err> addPet(@Body Object body);
}
- RawBodyConverter -
RawBody
request/response bodiesAResponse<RawBody, ErrorModel> addPet(RawBody body);
- ByteArrayConverter -
Byte[]/byte[]
request/response bodiesAResponse<Byte[], ErrorModel> addPet(Byte[] body);
- FileConverter - read/write
File
request/response bodiesAResponse<File, ErrorModel> addPet(File body);
- ResourceFileConverter - read resource
File
request body (not allowed for response body)AResponse<Pet, ErrorModel> addPet(ResourceFile body);
Interceptor allows you to store multiple request, response and exception actions (handlers).
The main feature of this interceptor is the ability to control the sequence of Actions calls for requests and responses,
which is not available in the base retrofit implementation. In other words, you yourself choose the order of the applied
Actions separately for Request and Response.
Action can implement three interfaces:
RequestInterceptAction
for processingokhttp3.Chain
andokhttp3.Request
ResponseInterceptAction
for processingokhttp3.Response
andjava.lang.Throwable
(network errors)InterceptAction
extendsRequestInterceptAction
andResponseInterceptAction
public class CustomCompositeInterceptor extends CompositeInterceptor {
public CustomCompositeInterceptor() {
super(LoggerFactory.getLogger(CustomCompositeInterceptor.class));
withRequestInterceptActionsChain(AuthAction.INSTANCE, LoggingAction.INSTANCE, AllureAction.INSTANCE);
withResponseInterceptActionsChain(LoggingAction.INSTANCE, AllureAction.INSTANCE);
}
}
Built-in Actions:
LoggingAction
- logs request/response or transport error (see logging implementation in the example module)CookieAction
- managing cookies headers on a threadAllureAction
- add request/response attachments to step
You can create your own simple QueryMap for queries by inheriting from ReflectQueryMap which allows you to read key-value pairs from class variables. This mechanism is in addition to the standard reading of parameters from the Map
.
Client for LoginUserQueryMap
examples
public interface UserApi {
@GET("/v2/user/login")
AResponse<Auth, Err> loginUser(@QueryMap() LoginUserQueryMap queryMap);
}
Reading values from variables through reflection.
@lombok.Getter
@lombok.Setter
@lombok.experimental.Accessors(chain = true, fluent = true)
public class LoginUserQueryMap extends ReflectQueryMap {
public static final LoginUserQueryMap ADMIN = new LoginUserQueryMap().username("test").password("abc123");
private Object username;
private Object password;
}
@QueryMapParameterRules(caseRule = SNAKE_CASE) // by rule
public class LoginUserQueryMap extends ReflectQueryMap {
@QueryMapParameter(name = "lastName") // explicit name
private Object username;
// <...>
}
@QueryMapParameterRules(nullRule = RULE_NULL_MARKER) // for all class variables
public class LoginUserQueryMap extends ReflectQueryMap {
@QueryMapParameter(nullRule = RULE_EMPTY_STRING) // only for a specific variable
private Object password;
// <...>
}
- RULE_IGNORE - Ignore null value parameters
- RULE_NULL_MARKER - replace null to null marker ->
/api/call?foo=%00
- RULE_EMPTY_STRING - replace null to empty string ->
/api/call?foo=
- RULE_NULL_STRING - replace null to null string ->
/api/call?foo=null
- CAMEL_CASE - camelCase
- KEBAB_CASE - kebab-case
- SNAKE_CASE - snake_case
- DOT_CASE - dot.case
- PASCAL_CASE - PascalCase
The current implementation of converters allows you to use Object as a @Body, which allows you to send as a request body
an object of any type that is supported by GsonConverterFactory
or JacksonConverterFactory
. The mechanism works for
any inheritors of the ExtensionConverterFactory
class. It is important to use the @Headers
annotation to
automatically select the correct converter based on the Content-Type header.
Client
public interface PetApi {
/**
* @param pet - Pet model that needs to be added to the store (required)
*/
@POST("/v2/pet")
@Headers({"Content-Type: application/json"})
@Description("Add a new pet to the store")
AResponse<Pet, Err> addPet(@Body Object pet);
// ^^^^^^
}
Request body (jackson/gson converter)
public class AddPetTests extends BasePetTest {
@Test
public void test1640455066880() {
PET_API.addPet(new Pet().name("fooBar")); // body -> {"name":"fooBar"}
// or
PET_API.addPet("fooBar"); // body -> "fooBar"
// or
PET_API.addPet(new RawBody("fooBar")); // body -> fooBar
// or
PET_API.addPet(true); // body -> true
// or
PET_API.addPet(ExtensionConverter.NULL_JSON_VALUE); // body -> <no body>
// or
final File file = new File("src/test/java/transport/data/PetPositive.json");
PET_API.addPet(file); // body -> <from file>
// or
final ResourceFile resourceFile = new ResourceFile("PetPositive.json");
PET_API.addPet(resourceFile); // body -> <from project resource file>
}
}
The response return type can be presented in the client interface in three options
Pet getPet(String id);
wherePet
is a converted response body. If the API call is completed with the unsuccessful HTTP status code, then the method will returnnull
.DualResponse<Pet, Err> getPet(String id);
WherePet
is a converted response body in case of success, andErr
in case of error.AResponse<Pet, Err> getPet(String id);
The same asDualResponse
only with the integration with the Allure framework.
DualResponse
and AResponse
inherited from BaseDualResponse
and includes the following methods:
assertResponse(Consumer<ASSERTER> respAsserter)
- to check the response;assertSucResponse(BiConsumer<ASSERTER, SUC_DTO> respAsserter, SUC_DTO expected)
- to check the success response;assertErrResponse(BiConsumer<ASSERTER, ERR_DTO> respAsserter, ERR_DTO expected)
- to check the error response;getErrDTO()
- return error response body model (nullable);getSucDTO()
- return success response body model (nullable);getEndpointInfo()
- return API method call info;getResponse()
- return rawokhttp3.Response
with readable body;getCallAnnotations()
- return called API method annotations;
Examples of response assertions can be viewed in classes:
veslo.example.ExampleApiClientAssertions
org.touchbit.retrofit.veslo.example.model.pet.Pet
For clarity:
public class ExampleTest {
@Test
public void test1640068360491() {
final Pet expected = addRandomPet();
PET_API.getPetById(expected.id()).assertResponse(asserter -> asserter
.assertHttpStatusCodeIs(200)
.assertSucBody(actual -> actual.assertPet(expected)));
}
@Test
public void test1640460353980() {
final Pet expected = generatePet();
PET_API.addPet(expected).assertSucResponse(Pet::assertPetResponse, expected);
}
@Test
public void test1640059907623() {
PET_API.getPetById(-1).assertErrResponse(this::assertStatus404, PET_NOT_FOUND);
}
}
DualResponse
contains built-in asserters that can be expanded and override. To do this, you need to
implement IResponseAsserter
and/or IHeadersAsserter
and create your CustomResponse
that should be inherited
from BaseDualResponse
. For clarity:
Next, when creating retrifit
client, you need to explicitly specify which response should be used when creating an
instance of IDualResponse
. Example for new Retrofit.Builder()
:
.addCallAdapterFactory(new UniversalCallAdapterFactory(CustomResponse::new))
- for default call adapter factory.addCallAdapterFactory(new AllureCallAdapterFactory(CustomResponse::new))
- for allure call adapter factory
Contains general methods for checking the response headers:
assertHeaderNotPresent(String headerName)
assertHeaderIsPresent(String headerName)
assertHeaderIs(String headerName, String expected)
assertHeaderContains(String headerName, String expected)
Provides similar methods for assert headers:
- Access-Control-Allow-Origin
- Connection
- Content-Type
- Etag
- Keep-Alive
- Server
- Set-Cookie
- Content-Encoding
- Transfer-Encoding
- Vary
Provides methods for checking HTTP status, Successful/Error response body.
Full description of methods looking in the class veslo.asserter.ResponseAsserter
The interface provides the ability to accumulate errors when asserts failed
public class Pet {
private Long id;
private Category category;
public void assertPet(Pet expected) {
try (SoftlyAsserter asserter = SoftlyAsserter.get()) {
asserter.ignoreNPE(true); // ignore NPE if this.category() return null (row 2)
asserter.softly(() -> assertThat(this.category()).as("Pet.category").isNotNull()); // 1
asserter.softly(() -> this.category().assertCategory(asserter, expected.category())); // 2
asserter.softly(() -> assertThat(this.id()).as("Pet.id").isEqualTo(expected.id())); // 3
}
// auto-closing throw exception if there are errors
}
}
Raw request/response body. Stores the body in the byte representation.
It is more suitable for requests violate the contract. For example, broken JSON.
Sensitive to No Content
responses (HTTP status code 204/205)
Contains built-in checks:
assertBodyIsNotNull()
assertBodyIsNull()
assertBodyIsNotEmpty()
assertBodyIsEmpty()
assertStringBodyContains(String... expectedStrings)
assertStringBodyContainsIgnoreCase(String... expectedStrings)
assertStringBodyIs(String expected)
assertStringBodyIsIgnoreCase(String expected)
Provides the ability to read files from project resources.
Added to more convenient use in the API requests.
PET_API.addPet(new ResourceFile("PetPositive.json"));
Allows you to handle the case when the response from the server contains extra fields without throwing an error when
converting (unlike Gson). Contains a method for checking the absence of extra fields assertNoAdditionalProperties()
.
public class Pet extends JacksonModelAdditionalProperties<Pet> {
private Long id;
}
If the contract has changed and a response has come with new fields, then we can clearly check that the response body does not contain extra fields without conversion errors.
The presence of extra fields in the model: Pet
Expected: no extra fields
Actual: {name=Puffy}
It is recommended to move the checks of the contract into separate tests.
public class AddPetTests {
@Test
@DisplayName("Pet model complies with API contract")
public void test1640455066880() {
PET_API.addPet(generatePet()).assertResponse(response -> response
.assertHttpStatusCodeIs(200)
.assertSucBody(Pet::assertNoAdditionalProperties));
}
}
Inheriting models from this interface allows you to validate data for compliance with a contract using JSR 303
annotations. Contains method for assert data consistency assertConsistency()
.
It is recommended to move the checks of the contract into separate tests.
public class AddPetTests {
@Test
@DisplayName("Pet model complies with API contract")
public void test1640455066880() {
PET_API.addPet(generatePet()).assertResponse(response -> response
.assertHttpStatusCodeIs(200)
.assertSucBody(Pet::assertConsistency));
}
}
For clarity:
Retrofit Veslo
Copyright 2021-2022 Retrofit Veslo project authors and contributors.