Разработка по контракту – это методология, при которой мы сначала создаем проектный документ (или определение интерфейса), который формально описывает API REST, который мы собираемся реализовать, прежде чем писать какой-либо реальный прототип. Эта методология полезна для четкого представления структуры и возможностей предстоящего API, прежде чем мы его внедрим. Получив формальное описание API с использованием языка определения, потребители нашего API могут начать создавать клиентов или даже протестировать его на сгенерированных макетных серверах, если захотят. Таким образом, разработка по контракту повышает независимость коворкинговых команд. На этапе проектирования определение интерфейса может служить общим артефактом проектирования, так что оно становится взаимосогласованным контрактом между разработчиками и потребителями конечных точек.
OpenAPI (ранее Swagger) – не единственный, но наиболее часто используемый существующий язык разметки REST API. Это независимый от языка формат на основе YAML для описания конечных точек, типов контента, полезных нагрузок запросов и ответов API. Те из нас, кто работает в команде Центра управления Hazelcast, используют документы Open API для разработки и анализа наших конечных точек REST, а также для обмена изменениями между внешними и внутренними разработчиками. С точки зрения SOAP, вы можете рассматривать файл OpenAPI как эквивалент документа WSDL.
В этом посте мы продемонстрируем, как можно использовать OpenAPI в интеграционном тесте на основе Будьте уверены , чтобы убедиться, что API соответствует его спецификации.
Определение и внедрение API
Для начала давайте определим API, который мы собираемся внедрить и протестировать в этой статье: человек-api.yaml :
openapi: '3.0.2'
info:
title: 'Person API for testing with RestAssured and OpenAPI4J'
version: '1.0'
paths:
/person:
post:
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Person'
responses:
200:
description: success
components:
schemas:
Person:
type: object
properties:
name:
type: string
age:
type: integer
minimum: 0
emails:
type: array
items:
type: string
minItems: 1
required:
- name
- age
- emails
additionalProperties: false
Эта разметка определяет конечную точку POST/person с телом запроса в виде объекта JSON, а его схема описывается с использованием схемы JSON. В этой статье мы не будем вдаваться в подробности языка схем JSON. В двух словах, как вы можете видеть, тело запроса должно быть объектом с 3 свойствами, имя , возраст и электронные письма .
Пока все хорошо, как только это определение открытого API будет завершено, вы сможете передать его другим командам, использующим сервис, и вы сможете использовать это определение в качестве общего источника истины.
Теперь давайте перейдем к (намеренно скелетному) Реализация API на основе Spring Boot:
PersonController.java :
@RestController
public class PersonController {
private Map storage = new ConcurrentHashMap<>();
@PostMapping("/persons")
public void create(@RequestBody PersonModel person) {
storage.put(person.getUserName(), person);
}
}
PersonModel.java :
@Data
public class PersonModel {
private String userName;
private int age;
private String emails;
}
Кроме того, давайте напишем интеграционный тест с использованием RestAssured для проверки конечной точки:
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class PersonControllerIntegrationTest {
@LocalServerPort
int port;
@Test
public void testCreatePerson() {
given().port(port)
.contentType("application/json")
.when().body("{\"userName\":\"John Doe\", \"age\":20, \"emails\":\"johndoe@example.com\"}")
.post("/persons")
.then().statusCode(200);
}
}
Пока все хорошо. API выглядит хорошо, и мы добавили тест для сценария счастливого пути, чтобы убедиться, что все будет работать так, как ожидалось в производстве. Но… подожди, что?
Если вы сравните реализованный API с предыдущей спецификацией Open API, то обнаружите ряд несоответствий, затем вы можете найти ряд несоответствий между ними:
- путь к конечной точке –
/человекв спецификации открытого API, в то время как это/человек - в процессе реализации
имя пользователя представлено свойствомимяв спецификации, в то время как оноимя пользователя - в процессе реализации
электронные письма
Это те ошибки, которые нелегко обнаружить при изолированном тестировании службы, и они обнаруживаются только тогда, когда мы используем конечную точку для интеграции с потребляющими службами (в худшем случае: только в производстве). Итак, как мы можем предотвратить эти проблемы на ранней стадии тестирования? Как мы можем убедиться, что API, который мы фактически реализуем, соответствует спецификации, которой мы поделились с нашими потребителями?
Улучшение теста с помощью проверки на основе открытого API
Мы можем встроить наше определение открытого API в набор интеграционных тестов. Давайте внедрим фильтр с повторной проверкой, который проверяет каждый запрос, отправленный нашим тестом, а также ответы, полученные от службы, на соответствие person-api.yaml , содержащему наше определение. Учитывая, что OpenAPI имеет реализацию на основе Java, называемую OpenAPI4J , и у нее есть адаптер, который помогает проверять объекты запроса и ответа RestAssured, довольно просто собрать фильтр RestAssured, который проверяет фактические пары запрос-ответ на соответствие спецификации (отказ от ответственности: адаптер RestAssured OpenAPI4J был разработан мной).
OpenApiValidationFilter.java :
public class OpenApiValidationFilter
implements Filter {
private static final Map> METHOD_TO_OPERATION = Map.of(
"GET", Path::getGet, "PUT", Path::getPut,
"POST", Path::getPost, "DELETE", Path::getDelete,
"OPTIONS", Path::getOptions, "HEAD", Path::getHead,
"PATCH", Path::getPatch, "TRACE", Path::getTrace
);
private static OpenApi3 parse(String yamlResourcePath) {
try {
return new OpenApi3Parser().parse(OpenApiValidationFilter.class.getResource(yamlResourcePath), true);
} catch (ResolutionException | ValidationException e) {
throw new RuntimeException(e);
}
}
private final OpenApi3 api;
public OpenApiValidationFilter(String yamlResourcePath) {
this(parse(yamlResourcePath));
}
public OpenApiValidationFilter(OpenApi3 api) {
this.api = api;
}
@SneakyThrows
@Override
public Response filter(FilterableRequestSpecification requestSpec, FilterableResponseSpecification responseSpec,
FilterContext ctx) {
try {
ValidationResults results = OpenApi3Validator.instance().validate(api);
if (results.size() > 0) {
throw new RuntimeException("invalid OpenAPI definition: " + results);
}
RestAssuredRequest request = new RestAssuredRequest(requestSpec);
RequestValidator validator = new RequestValidator(api);
validator.validate(request);
Response response = ctx.next(requestSpec, responseSpec);
Path path = getOpenApiPathByRequest(request);
validator.validate(new RestAssuredResponse(response), path,
METHOD_TO_OPERATION.get(requestSpec.getMethod()).apply(path));
return response;
} catch (ValidationException e) {
throw new AssertionError(e);
}
}
public Path getOpenApiPathByRequest(RestAssuredRequest request) {
Map patterns = new HashMap<>();
for (Map.Entry pathEntry : api.getPaths().entrySet()) {
List builtPathPatterns = PathResolver.instance().buildPathPatterns(
api.getContext(),
api.getServers(),
pathEntry.getKey());
for (Pattern pathPattern : builtPathPatterns) {
patterns.put(pathPattern, pathEntry.getValue());
}
}
Pattern pathPattern = PathResolver.instance().findPathPattern(patterns.keySet(), request.getPath());
Path p = patterns.get(pathPattern);
return p;
}
}
Наконец, давайте изменим ранее написанный интеграционный тест, чтобы использовать фильтр для перехвата всех запросов и ответов:
@Test
public void testCreatePerson(){
given().port(port)
// setting up the filter
.filter(new OpenApiValidationFilter("/person-api.yaml"))
.contentType("application/json")
.when().body("{\\"userName\\":\\"John Doe\\", \\"age\\":20, \\"emails\\":[]}")
.post("/persons")
.then().statusCode(200);
}
Учитывая этот фильтр, теперь, если мы запустим тест, мы получим следующую ошибку: java.lang. Ошибка утверждения: Путь к операции не найден по URL-адресу 'http://localhost:43979/persons ' .
Как только мы изменим путь запроса как в контроллере, так и в тесте на/человека, мы получим больше ошибок, связанных с несоответствиями схемы тела запроса:
java.lang.AssertionError: Invalid request. Validation error(s) : body: Additional property 'userName' is not allowed. (code: 1000) From: body.body.emails: Type expected 'array', found 'string'. (code: 1027) From: body.emails. body: Field 'name' is required. (code: 1026) From: body. .
После устранения этих ошибок проверки тест будет пройден.
Резюме
- Разработка на основе контракта заключается в использовании открытого API или аналогичного языка определения интерфейса для формального описания реализуемого API
- определение открытого API может использоваться в качестве общего источника информации между командами, определяющего и документирующего API
- без дополнительных усилий реализованный API может не соответствовать указанному интерфейсу, что приведет к ошибкам взаимодействия между службами, которые могут быть утомительными для отладки
- Будьте уверены, и openapi4j вместе могут помочь выявить такие проблемы во время интеграционного тестирования, проверив, что каждый запрос и ответ соответствуют определению OpenAPI
Ресурсы
Все примеры кода, представленные в этом посте, доступны в этот репозиторий .
Оригинал: “https://dev.to/erosb/contract-first-development-using-restassured-and-openapi-11om”