
В статье речь пойдет об интеграции веб-приложений, написанных с помощью 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/
Добавить комментарий