Демонстрационный проект #
https://git.crtweb.ru/creative.qa/anarchy/mag/restassured-best-practices
Используйте спецификации #
Конструкция типа
given()
.accept(ContentType.JSON)
используется абсолютно в каждом запросе, и, даже если это всего две строчки, их можно сократить до вызова одного метода, увеличив тем самым читаемость кода, если вынести их в отдельный метод:
public static RequestSpecification requestSpec() {
return given()
.accept(ContentType.JSON);
}
или билдер:
RequestSpecification requestSpec = new RequestSpecBuilder()
.setAccept(ContentType.JSON)
.build();
А в каждом запросе достаточно использовать только вызов этого метода или билдера.
Кроме того, на деле вряд ли весь код, который можно было бы вынести в спецификацию запроса ограничивается двумя строками – наверняка туда можно добавить заголовки для авторизации, установку хоста и порта, настройки логирования, фильтры для Allure и что угодно еще, что необходимо выполнять при каждом запросе.
То же самое можно сделать и с ответом. В нем можно проверить статус-код, время ожидания ответа, а так же наличие необходимых полей в теле ответа или заголовков. Как и в случае с запросом, можно использовать метод:
public static ValidatableResponse responseSpecOk(Response response) {
return response
.then()
.statusCode(200)
.time(lessThanOrEqualTo(responseTime));
}
или билдер:
ResponseSpecification responseSpecOk = new ResponseSpecBuilder()
.expectStatusCode(200)
.expectResponseTime(lessThanOrEqualTo(responseTime))
.build();
Храните эндпойнты в отдельном месте #
Не стоит отдельно прописывать эндпойнт для каждого запроса. Даже если их немного, при необходимости внесения изменений что-то все равно может потеряться и поиск проблемы гарантированно займет куда больше времени, чем организация корректного хранения эндпойнтов.
Тут есть несколько вариантов:
-
если для каждого эндпойнта существует свой собственный класс, можно просто хранить их в переменных этого класса;
-
можно создать отдельный класс для управления эндпойнтами, и хранить все эндпойнты в переменных этого класса;
-
и, наконец, эндпойнты можно хранить в отдельном property-файле.
В случае, если эндпойнт включает в себя какое-то изменяемое значение (например, ID), можно пользоваться записью такого вида:
String endpoint = endpoint/{id}
В таком случае, подставить на место значения в фигурных скобках нужную переменную можно будет следующим образом:
.get(endpoint, id)
Однако стоит иметь в виду, что подобная запись не всегда работает корректно, так что в некоторых проектах, возможно, придется обходиться без нее.
Выносите base URI в properties #
Так же, как и упомянутые выше заголовки, скорее всего часть URL повторяется в каждом запросе. Ее также можно вынести в отдельное место и объявить только один раз.
Для этого удобнее всего вынести baseURI в properties-файл и получать его значение через класс типа ManageProperties. Объявить baseURI можно двумя способами:
given()
.baseUri(getProperty("baseUri"))
или
RestAssured.baseURI = getProperty("baseUri");
Что касается непосредственно самого вызова baseURI, тут тоже есть пара вариантов:
Первый – вызывать его в каждом запросе. Такой вариант выглядит более громоздко, но подойдет в том случае, если в определенных случаях нужно использовать другой base URI.
Второй – объявить его однократно, например, в спецификации запроса.
Используйте функционал Rest Assured для преобразования объектов #
Нет необходимости преобразовывать POJO в Json с помощью возможностей внешних библиотек, так как Rest Assured прекрасно справляется с этим самостоятельно. Достаточно добавить нужный POJO в тело запроса:
Pojo pojo = new Pojo();
given()
.body(pojo)
.post(URI);
Обратный процесс также работает:
Pojo pojo = given()
.get(URI)
.then
.extract().body().as(Pojo.class)
Кроме того, Rest Assured умеет преобразовывать в Json и HashMap, в том числе и вложенные:
Map <Object, Object> outerMap = new HashMap<Object, Object>();
Map <Object, Object> innerMap = new HashMap<Object, Object>();
innerMap.put("key", "value");
outerMap.put("key", "value");
outerMap.put("innerMap", innerMap);
given()
.body(outerMap)
.post(URI);
Используйте валидацию Json-схемы #
Валидация Json-схемы - это простой и надежный способ убедиться, что полученные данные соответствуют ожидаемому формату. Json-схема имеет следующий вид:
{
"type": "object",
"properties": {
"id": {
"type": "integer"
},
"item": {
"type": "object",
"properties": {
"key1": {
"type": ["string", "integer"]
},
"key2": {
"type": "string"
}
},
"required": ["key1", "key2"]
}
},
"required": ["id", "item"]
}
Создавать Json-схемы можно и вручную, но из-за не слишком человекоориентированного формата это может быть довольно утомительно, так что куда проще использовать для этого любой из многочисленных автоматических инструментов. Например: https://www.liquid-technologies.com/online-json-to-schema-converter
Такая схема проверяет, что полученный в ответе Json содержит в себе все поля, которые указаны как обязательные, и что тип данных в каждом из этих полей соответствует ожидаемому.
Для использования валидации Json-схем понадобится установить библиотеку json-schema-validator (в данном случае используется библиотека от io.rest-assured) и использовать следующий метод:
.body(matchesJsonSchemaInClasspath("schema.json"));
Выносите повторяющиеся действия в отдельные методы #
Нет никакой необходимости десять раз проверять, что ответ на запрос в десяти тестах соответствует схеме, достаточно один раз написать отдельный метод, который проводит эту проверку, и в дальнейшем просто обращаться к нему.
Это справедливо и для любых повторяющихся действий – намного удобнее читать (и тем более писать!) код, в котором все спецификации, конструкторы и проверки разбиты на отдельные блоки, из которых можно создать нужные тесты. Например, вот так:
public static RequestSpecification requestSpec = new RequestSpecBuilder()
.setBaseUri(BASE_URI)
.setContentType(ContentType.JSON)
.setAccept(ContentType.JSON)
.build();
public static ResponseSpecification responseSpec(int statusCode) {
return new ResponseSpecBuilder()
.expectStatusCode(statusCode)
.expectResponseTime(lessThanOrEqualTo(responseTime))
.build();
}
public static Response createItem(Pojo body) {
return given()
.spec(requestSpec)
.body(body)
.post(Uri);
}
public static ValidatableResponse createItem_valid(Response response) {
return response
.then()
.spec(responseSpec(200))
.body(matchesJsonSchemaInClasspath("schemas/CreateItem.json"));
}
@Test
public void createItemTest() {
createItem_valid(
createItem(body))
}
Используйте Hamcrest Matchers #
В случе, когда нет необходимости десериализовать ответ в POJO, мэтчеры из библиотеки Hamcrest являются самым быстрым способом повести валидацию отдельных элементов содержимого ответа.
Они позволяют проверить, что, например, ответ содержит в себе определенную строку или значение, или что конкретное поле равно определенному значению, или что массив содержит в себе некоторый набор элементов в произвольном порядке.
.body("id", equals(getId()))
.body("id", greaterThanOrEqualTo(1))
.body("items", either(hasItem("a")).and(hasItem("b"))
Все выполняемые мэтчерами проверки, несомненно, можно произвести и другими способами, однако они позволяют сделать это в краткой и удобной для чтения форме.
С полной документацией к мэтчерам можно ознакомиться здесь: http://hamcrest.org/JavaHamcrest/javadoc/1.3/org/hamcrest/Matchers.html
Используйте методы для добавления заголовков #
Так как по крайней мере один из этих заголовков используется практически в любом запросе, для них был создан более краткий вариант записи:
given()
.contentType(ContentType.JSON)
.accept(ContentType.JSON)
Такой вариант немного короче обычной записи заголовков через метод header и, что более важно, минимизирует шанс возникновения ошибки из-за опечатки.
Однако стоит обратить внимание на то, что при таком оформлении заголовков в Accept помимо application/json также передаются application/javascript, text/javascript и text/json. В некоторых API это может привести к ошибкам, и в таком случае enum ContentType стоит заменить на строку с указанием конкретного типа данных.
Проверяйте статус-код без assertThat #
Запись типа
.assertThat(statusCode(200))
просто излишня. Эта строка выполняет ту же самую работу:
.statusCode(200)
Передавайте статус-коды в качестве аргумента #
При наличии большого количества негативных проверок с разными ожидаемыми кодами ошибок, нет необходимости писать отдельную спецификацию под каждый статус-код, вместо этого можно просто добавить спецификацию в метод и передавать в нее ожидаемый статус-код из теста:
public static ResponseSpecification responseSpecError(int statusCode) {
return new ResponseSpecBuilder()
.expectStatusCode(statusCode)
.expectResponseTime(lessThanOrEqualTo(responseTime))
.build();
}
Используйте логирование для отладки тестов #
Метод .log() позволяет получить информацию как об отправленном запросе, так и о полученном ответе. В первом случае необходимо его применить до отправки запроса (get, post и т.д.), во втором случае – после.
Вам доступны следующие методы:
.log().all()
.log().params()
.log().body()
.log().headers()
.log().cookies()
Однако оставлять логирование в тестах, которые уже дописаны и корректно работают, не стоит – объем в информации в логах может быть настолько большим, что результаты всех тестов перестанут умещаться в консоли IDE, не говоря уже о том, что просматривать результаты тестов в таком случае будет очень неудобно.