Автор оригинала: Dhananjay Singh.
Обзор
В этой статье мы познакомим вас с Контрактом Spring Cloud , который является ответом Spring на Контракты , ориентированные на потребителей .
В настоящее время приложения тщательно тестируются – будь то модульные тесты, интеграционные тесты или сквозные тесты. В архитектуре микросервисов очень часто служба ( потребитель ) взаимодействует с другой службой ( производителем ) для выполнения запроса.
Чтобы проверить их, у нас есть два варианта:
- Разверните все микросервисы и выполните сквозные тесты, используя библиотеку, такую как Selenium
- Пишите интеграционные тесты, издеваясь над вызовами других служб
Если мы воспользуемся первым подходом, мы будем имитировать производственную среду. Это потребует дополнительной инфраструктуры, и обратная связь будет запоздалой, так как для ее запуска потребуется много времени.
Если мы воспользуемся последним подходом, у нас будет более быстрая обратная связь, но поскольку мы высмеиваем ответы на внешние вызовы, насмешки не будут отражать изменения в производителе, если таковые имеются.
Например, предположим, что мы имитируем вызов внешней службы, которая возвращает JSON с ключом, скажем, имя
. Наши тесты проходят, и все работает нормально. Со временем другая служба изменила ключ на имя
.
Наши интеграционные тестовые примеры по-прежнему будут работать просто отлично. Проблема, скорее всего, будет замечена в промежуточной или производственной среде, а не в сложных тестовых случаях.
Spring Cloud Contract предоставляет нам верификатор Spring Cloud Contract/| именно для этих случаев. Он создает заглушку из службы производителя, которую служба потребителей может использовать для подделки вызовов.
Поскольку заглушка имеет версию в соответствии со службой производителя, служба потребителей может выбрать, какую версию выбрать для тестирования. Это обеспечивает более быструю обратную связь и гарантирует, что наши тесты действительно отражают код.
Установка
Чтобы продемонстрировать концепцию контрактов, у нас есть следующие базовые услуги:
- spring-cloud-контракт-производитель : простая служба REST , имеющая единственную конечную точку
/сотрудник/{идентификатор}
, которая выдает ответ JSON. - spring-cloud-контракт-потребитель : простой клиент-потребитель, который вызывает
/сотрудника/{идентификатор}
конечную точкуspring-cloud-контракт-производителя
для завершения ответа.
Чтобы сосредоточиться на этой теме, мы будем использовать только эти службы , а не другие службы , такие как Eureka, Gateway и т.д., Которые обычно включены в архитектуру микросервиса.
Детали настройки производителя
Давайте начнем с простого класса POJO – Сотрудник
:
public class Employee { public Integer id; public String fname; public String lname; public Double salary; public String gender; // Getters and setters
Затем у нас есть EmployeeController
с одним GET
отображением:
@RestController public class EmployeeController { @Autowired EmployeeService employeeService; @GetMapping(value = "employee/{id}") public ResponseEntity> getEmployee(@PathVariable("id") int id) { Optionalemployee = employeeService.findById(id); if (employee.isPresent()) { return ResponseEntity.status(HttpStatus.OK).contentType(MediaType.APPLICATION_JSON).body(employee.get()); } else { return ResponseEntity.status(HttpStatus.NOT_FOUND).build(); } } }
Это простой контроллер, который возвращает Employee
JSON со всеми атрибутами класса в качестве ключей JSON на основе id
.
Служба сотрудников
может быть чем угодно , что находит сотрудника по идентификатору
, в нашем случае это простая реализация JpaRepository
:
public interface EmployeeService extends JpaRepository{}
Детали настройки потребителя
Со стороны потребителя давайте определим еще одного POJO – Человека
:
class Person { private int id; public String fname; public String lname; // Getters and setters
Обратите внимание, что имя класса не имеет значения, если имена атрибутов одинаковы – id
, fname
и lname
.
Теперь предположим, что у нас есть компонент, который вызывает /сотрудника/{id}
конечную точку spring-cloud-контрактного производителя
:
@Component class ConsumerClient { public Person getPerson(final int id) { final RestTemplate restTemplate = new RestTemplate(); final ResponseEntityresult = restTemplate.exchange("http://localhost:8081/employee/" + id, HttpMethod.GET, null, Person.class); return result.getBody(); } }
Поскольку Человек
класс из spring-cloud-контракт-потребитель
имеет те же имена атрибутов, что и Сотрудник
класс из spring-cloud-контракт-производитель
– Spring автоматически сопоставит соответствующие поля и предоставит нам результат.
Тестирование потребителя
Теперь, если бы мы хотели протестировать потребительский сервис, мы бы провели пробный тест:
Git Essentials
Ознакомьтесь с этим практическим руководством по изучению Git, содержащим лучшие практики и принятые в отрасли стандарты. Прекратите гуглить команды Git и на самом деле изучите это!
@SpringBootTest(classes = SpringCloudContractConsumerApplication.class) @RunWith(SpringRunner.class) @AutoConfigureWireMock(port = 8081) @AutoConfigureJson public class ConsumerTestUnit { @Autowired ConsumerClient consumerClient; @Autowired ObjectMapper objectMapper; @Test public void clientShouldRetrunPersonForGivenID() throws Exception { WireMock.stubFor(WireMock.get(WireMock.urlEqualTo("/employee/1")).willReturn( WireMock.aResponse() .withStatus(200) .withHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_UTF8_VALUE) .withBody(jsonForPerson(new Person(1, "Jane", "Doe"))))); BDDAssertions.then(this.consumerClient.getPerson(1).getFname()).isEqualTo("Jane"); } private String jsonForPerson(final Person person) throws Exception { return objectMapper.writeValueAsString(person); } }
Здесь мы высмеиваем результат конечной точки /employee/1
, чтобы вернуть жестко закодированный ответ JSON, а затем продолжаем наше утверждение.
Итак, что произойдет, если мы что-то изменим в производителе?
Код, который проверяет потребителя, не будет отражать это изменение.
Реализация Весеннего Облачного Контракта
Чтобы убедиться, что эти услуги находятся “на одной странице”, когда дело доходит до изменений, мы предоставляем им обоим контракт, как и людям.
Когда служба производителя изменяется, для службы потребителей создается заглушка /| квитанция , чтобы сообщить ей, что происходит.
Контракт на обслуживание Производителя
Чтобы реализовать это, сначала давайте добавим зависимость//spring-cloud-starter-contract-verifier
в нашего производителя pom.xml
:
org.springframework.cloud spring-cloud-starter-contract-verifier test
Теперь нам нужно определить контракт , на основе которого контракт Spring Cloud будет выполнять тесты и создавать заглушку . Это делается с помощью spring-cloud-starter-contract-верификатора
, который поставляется с Языком определения контракта (DSL), написанным на Groovy или YAML.
Давайте создадим контракт, используя Groovy в новом файле – должен вернуть сотрудника, когда идентификатор сотрудника найден.groovy
:
import org.springframework.cloud.contract.spec.Contract Contract.make { description("When a GET request with an Employee id=1 is made, the Employee object is returned") request { method 'GET' url '/employee/1' } response { status 200 body(""" { "id": "1", "fname": "Jane", "lname": "Doe", "salary": "123000.00", "gender": "M" } """) headers { contentType(applicationJson()) } } }
Это довольно простой контракт, который определяет пару вещей. Если есть GET
запрос на URL /сотрудник/1
, верните ответ со статусом 200
и тело JSON с 5 атрибутами.
Когда приложение будет построено, на этапе тестирования, с помощью контракта Spring Cloud будут созданы автоматические тестовые классы, которые будут считываться с этого заводного файла.
Однако, чтобы сделать возможным автоматическое создание тестовых классов, нам нужно создать базовый класс, который они могут расширить. Чтобы зарегистрировать его в качестве базового класса для тестов, мы добавляем его в ваш pom.xml
файл:
org.springframework.cloud spring-cloud-contract-maven-plugin true com.mynotes.springcloud.contract.producer.BaseClass
Наш Базовый класс
выглядит примерно так:
@SpringBootTest(classes = SpringCloudContractProducerApplication.class) @RunWith(SpringRunner.class) public class BaseClass { @Autowired EmployeeController employeeController; @MockBean private EmployeeService employeeService; @Before public void before() { final Employee employee = new Employee(1, "Jane", "Doe", 123000.00, "M"); Mockito.when(this.employeeService.findById(1)).thenReturn(Optional.of(employee)); RestAssuredMockMvc.standaloneSetup(this.EmployeeController); } }
А теперь давайте создадим наше приложение:
$ mvn clean install
Наша папка target
, помимо обычных сборок, теперь также содержит заглушки
банку:
Поскольку мы выполнили установку
, он также доступен в нашей локальной папке .m2
. Этот заглушка теперь может использоваться нашим потребителем
spring-cloud-контракта//для издевательств над вызовами.
Договор на Бытовое Обслуживание
Как и на стороне производителя, нам также необходимо добавить определенный вид контракта к нашему потребительскому обслуживанию. Здесь нам нужно добавить spring-cloud-starter-contract-stub-runner
зависимость от вашего pom.xml
:
org.springframework.cloud spring-cloud-starter-contract-stub-runner test
Теперь, вместо того, чтобы издеваться над нашими местными издевательствами, мы можем скачать заглушки от производителя:
@SpringBootTest(classes = SpringCloudContractConsumerApplication.class) @RunWith(SpringRunner.class) public class ConsumerTestContract { @Rule public StubRunnerRule stubRunnerRule = new StubRunnerRule() .downloadStub("com.mynotes.spring-cloud", "spring-cloud-contract-producer", "0.0.1-SNAPSHOT", "stubs") .withPort(8081) .stubsMode(StubRunnerProperties.StubsMode.LOCAL); @Autowired ConsumerClient consumerClient; @Test public void clientShouldRetrunPersonForGivenID_checkFirsttName() throws Exception { BDDAssertions.then(this.consumerClient.getPerson(1).getFname()).isEqualTo("Jane"); } @Test public void clientShouldRetrunPersonForGivenID_checkLastName() throws Exception { BDDAssertions.then(this.consumerClient.getPerson(1).getLname()).isEqualTo("Doe"); } }
Как вы можете видеть, мы использовали заглушку, созданную spring-cloud-контрактным производителем
. .stubsMode()
должен указать Spring, где должна выглядеть зависимость от заглушки. ЛОКАЛЬНЫЙ
означает в локальной папке .m2
. Другими вариантами являются УДАЛЕННЫЙ
и ПУТЬ к КЛАССУ
.
Класс Потребительский тестовый контракт
сначала запустит заглушку, и из-за ее поставщика производителем мы не зависим от насмешек над внешним вызовом. Если предположить, что производитель действительно изменил контракт, можно быстро выяснить, с какой версии было внесено критическое изменение, и можно предпринять соответствующие шаги.
Вывод
Мы рассмотрели, как использовать Spring Cloud Contract, чтобы помочь нам поддерживать контракт между производителем и службой поддержки потребителей. Это достигается путем первого создания заглушки со стороны производителя с использованием заводного DSL. Этот сгенерированный заглушка может быть использован в службе поддержки потребителей для имитирования внешних вызовов.
Как всегда, код для примеров, используемых в этой статье, можно найти на GitHub .