Pull to refresh

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

Reading time4 min
Views10K


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


Речь пойдет об API контрактах.


Для юнит-тестирования контроллеров достаточно часто используются mockMVC или 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 контракта, который описан на человекопонятном языке, и из него мы генерируем юнит-тесты (теоретически еще и документацию), и из него же моки. Данный подход снижает риски ошибок интеграции между веб-приложениями.


Примеры можно посмотреть на официальном сайте например.


Примеры кода в статье взяты из простейшего проекта тут.

Tags:
Hubs:
+10
Comments0

Articles