Интеграция web-приложений с помощью Spring Cloud Contract

от автора

В статье речь пойдет об интеграции веб-приложений, написанных с помощью Spring и работающих по HTTP. Название Spring Cloud Contract, на мой взгляд, вводит в заблуждение, так как не имеет ничего общего с cloud.
Речь пойдет об API контрактах.

Для юнит-тестирования контроллеров достаточно часто используются mockMCV или RestAssured. Для моков на стороне фронтэнда используются моск-серверы, например Wiremock или Pact. Но зачастую, юнит-тесты пишут одни люди, а моки другие.
Это может привести к пролбемам при интеграции.
Например, сервер при отсутствии данных может возвращать 204 NO_CONTENT, а клиент может ожидать 200 OK и пустой json.
Тут не важно, кто из них прав. Проблема в том, что кто-то сделал ошибку и она будет найдена не раньше этапа интеграции.

Вот эту проблему и призван решить spring cloud contract.

Что такое spring cloud contract

Это файл, в котором на yaml или groovyDSL диалекте описано, как должны выглядеть запрос и ответ. По умолчанию все контракты лежат в папке /src/test/resources/contracts/*.

Для примера протестируем простейший GET-endpoint

@GetMapping("/bets/{userId}") public ResponseEntity<List<Bet>> getBets(@PathVariable("userId") String userId) {    List<Bet> bets = service.getByUserId(userId);    if (bets.isEmpty()) {        return ResponseEntity.noContent().build();    }    return ResponseEntity.ok(bets); }

Опишем контракт

org.springframework.cloud.contract.spec.Contract.make {    request {        method 'GET'        urlPath '/bets/2'    }    response {        status 200        headers {            header('Content-Type', 'application/json')        }        body('''          {            "sport":"football",            "amount": 1          }          '''        )    } } 

Далее из этого файла с помощью maven или gradle плагина генерируются юнит-тесты и json’ы для wiremock.
JSON описание мока для примера выше будет выглядеть так:

{  "id" : "df8f7b73-c242-4664-add3-7214ac6356ff",  "request" : {    "urlPath" : "/bets/2",    "method" : "GET"  },  "response" : {    "status" : 200,    "body" : "{\"amount\":1,\"sport\":\"football\"}",    "headers" : {      "Content-Type" : "application/json"    },    "transformers" : [ "response-template" ]  },  "uuid" : "df8f7b73-c242-4664-add3-7214ac6356ff" }

Wiremock можно запустить локально, надо только скачать jar отсюда. По умолчанию json-моки надо положить папку mappings.

$java -jar wiremock-standalone-2.18.0.jar 

Ниже показан сгеренированный тест. По умолчанию использована библиотека RestAssured, но могут быть использоаваны mockMVC или spockframework.

public class UserControllerTest extends ContractBae {    @Test   public void validate_get_200() throws Exception {      // given:         MockMvcRequestSpecification request = given();       // when:         ResponseOptions response = given().spec(request)               .get("/bets/2");       // then:         assertThat(response.statusCode()).isEqualTo(200);         assertThat(response.header("Content-Type")).isEqualTo("application/json");      // and:         DocumentContext parsedJson = JsonPath.parse(response.getBody().asString());         assertThatJson(parsedJson).field("['amount']").isEqualTo(1);         assertThatJson(parsedJson).field("['sport']").isEqualTo("football");   } }

Следует отметить, что все сгенерированные классы наследуют какой-нибудь базовый класс (базовых классов может быть несколько), в котором инициализируются все необходимые параметры для тестов. Путь к базовому классу описывается в настройках плагина.
Для данного примера базовый класс может выглядеть так:

@RunWith(SpringRunner.class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = SccDemoApplication.class) public abstract class ContractBae {    @LocalServerPort    int port;    @Autowired    protected WebApplicationContext webApplicationContext;     @Before    public void configureRestAssured() {        RestAssured.port = port;        MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext)                .build();        RestAssuredMockMvc.mockMvc(mockMvc);    } }

В итоге получаем, что и тесты и моки получены из одного источника. Если юнит-тесты проходят и условный фронтэнд работает на моках, то и с интеграцией проблем не будет.

Но это еще не все
Моки может использовать не только фронтэнд, но само приложение для интеграции с другим приложением. Спринг умеет запускать моск-сервер, надо только сгенерировать jar с моками и передать путь к нему аннотации @AutoConfigureStubRunner

Допустим что наш контроллер делает HTTP к другому приложению:

@GetMapping("/bets/{userId}") public ResponseEntity<List<Bet>> getBets(@PathVariable("userId") String userId) {    if(!isUsetExists(userId)) {        return ResponseEntity.notFound().build();    }    List<Bet> bets = service.getByUserId(userId);    if (bets.isEmpty()) {        return ResponseEntity.noContent().build();    }    return ResponseEntity.ok(bets); }  private boolean isUsetExists(String userId) {    try {        restTemplate.getForObject("/exists/" + userId, Void.class);        return true;    } catch (HttpStatusCodeException ignore) {        return false;    } }

Тогда надо просто описать пусть к jar с моками в базовом классе

@RunWith(SpringRunner.class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = SccDemoApplication.class) @AutoConfigureStubRunner(ids = {"me.dehasi.contracts.demo:sub-service-stubs:+:stubs:8090"}, stubsMode = StubRunnerProperties.StubsMode.LOCAL) public abstract class ContractBase {

Т.к. это тесты контроллера, то из этих же тестов можно сгенерировать ascii-doc сниппеты (полноценная статья про rest-docs уже есть на хабре) .

Что мы имеем
Получается, что у нас есть один источник API контракта, который описан на человекопонятном языке, и из него мы генерируем юнит-тесты (теоретически еще и документацию), и из него же моки. Данный подход снижает риски ошибок интеграции между веб-приложениями.

Примеры можно посмотреть на официальном сайте например
Примеры кода в статье взяты из простейшего проекта тут


ссылка на оригинал статьи https://habr.com/post/424057/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *