Я работаю над этой идеей, и я не знаю, нравится ли вам это, ребята. Я хотел бы узнать ваше мнение о том, стоит ли изучать это дальше.
Вот в чем дело: я столкнулся с командами, которые при работе с технологиями SOA были втянуты в грязь из-за сложности их инструментов. Я видел это только на Java, но слышал от некоторых разработчиков на C #, что они также распознают это явление. Я хотел бы изучить альтернативный подход.
Этот подход требует более тяжелой работы, чем добавление файла WSDL (язык определения веб-сервиса. Hocus pocus) в ваш проект и автоматическая генерация материала. Но это приходит с дополнительным пониманием и повышенной тестируемостью. В конце концов я понял, что это позволило мне быстрее выполнять свои задачи, несмотря на дополнительный ручной труд.
Цель этого поста (и, если вам нравится, это расширение) — изучить более простой подход к SOA в целом и к веб-сервисам в частности. Я иллюстрирую эти принципы на конкретном примере: пусть пользователи будут уведомлены, когда их валюта упадет ниже порога по отношению к доллару США. Чтобы сделать сервис технологически интересным, я буду использовать IP-адрес абонента для определения его валюты.
Шаг 1: Создайте свои активные сервисы, высмеивая внешние взаимодействия
Подчеркивание активности ваших собственных сервисов может помочь вам создать интерфейсы, которые определяют ваше взаимодействие с внешними сервисами.
Тизер:
public class CurrencyPublisherTest { private SubscriptionRepository subscriptionRepository = mock(SubscriptionRepository.class); private EmailService emailService = mock(EmailService.class); private CurrencyPublisher publisher = new CurrencyPublisher(); private CurrencyService currencyService = mock(CurrencyService.class); private GeolocationService geolocationService = mock(GeolocationService.class); @Test public void shouldPublishCurrency() throws Exception { Subscription subscription = TestDataFactory.randomSubscription(); String location = TestDataFactory.randomCountry(); String currency = TestDataFactory.randomCurrency(); double exchangeRate = subscription.getLowLimit() * 0.9; when(subscriptionRepository.findPendingSubscriptions()).thenReturn(Arrays.asList(subscription)); when(geolocationService.getCountryByIp(subscription.getIpAddress())).thenReturn(location); when(currencyService.getCurrency(location)).thenReturn(currency); when(currencyService.getExchangeRateFromUSD(currency)).thenReturn(exchangeRate); publisher.runPeriodically(); verify(emailService).publishCurrencyAlert(subscription, currency, exchangeRate); } @Before public void setupPublisher() { publisher.setSubscriptionRepository(subscriptionRepository); publisher.setGeolocationService(geolocationService); publisher.setCurrencyService(currencyService); publisher.setEmailService(emailService); } }
Спойлер: Я недавно начал генерировать случайные тестовые данные для своих тестов с большим эффектом.
Издатель имеет ряд сервисов, которые он использует. Давайте сосредоточимся сейчас на одном сервисе: GeoLocationService.
Шаг 2. Создайте тест и заглушку для каждого сервиса — начиная с GeoLocationService
Тест верхнего уровня показывает, что нам нужно от каждого внешнего сервиса. Получив информацию и прочитав (да!) WSDL для службы, мы можем протестировать заглушку для службы. В этом примере мы фактически запускаем тест с использованием HTTP, запуская Jetty, встроенный в тест.
Тизер:
public class GeolocationServiceStubHttpTest { @Test public void shouldAnswerCountry() throws Exception { GeolocationServiceStub stub = new GeolocationServiceStub(); stub.addLocation("80.203.105.247", "Norway"); Server server = new Server(0); ServletContextHandler context = new ServletContextHandler(); context.addServlet(new ServletHolder(stub), "/GeoService"); server.setHandler(context); server.start(); String url = "http://localhost:" + server.getConnectors()[0].getLocalPort(); GeolocationService wsClient = new GeolocationServiceWsClient(url + "/GeoService"); String location = wsClient.getCountryByIp("80.203.105.247"); assertThat(location).isEqualTo("Norway"); } }
Проверьте и создайте полезную нагрузку XML
Это первый «битый» бит. Здесь я создаю полезную нагрузку XML без использования фреймворка (отличный «$» — синтаксис предоставлен библиотекой JOOX , тонкой оболочкой поверх встроенных классов JAXP):
Я добавляю XSD (более сфокусированный фокус) для фактического сервиса в проект и код для проверки сообщения. Затем я начинаю создавать полезную нагрузку XML, следуя ошибкам проверки.
Тизер:
public class GeolocationServiceWsClient implements GeolocationService { private Validator validator; private UrlSoapEndpoint endpoint; public GeolocationServiceWsClient(String url) throws Exception { this.endpoint = new UrlSoapEndpoint(url); validator = createValidator(); } @Override public String getCountryByIp(String ipAddress) throws Exception { Element request = createGeoIpRequest(ipAddress); Document soapRequest = createSoapEnvelope(request); validateXml(soapRequest); Document soapResponse = endpoint.postRequest(getSOAPAction(), soapRequest); validateXml(soapResponse); return parseGeoIpResponse(soapResponse); } private void validateXml(Document soapMessage) throws Exception { validator.validate(toXmlSource(soapMessage)); } protected Validator createValidator() throws SAXException { SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); Schema schema = schemaFactory.newSchema(new Source[] { new StreamSource(getClass().getResource("/geoipservice.xsd").toExternalForm()), new StreamSource(getClass().getResource("/soap.xsd").toExternalForm()), }); return schema.newValidator(); } private Document createSoapEnvelope(Element request) throws Exception { return $("S:Envelope", $("S:Body", request)).document(); } private Element createGeoIpRequest(String ipAddress) throws Exception { return $("wsx:GetGeoIP", $("wsx:IPAddress", ipAddress)).get(0); } private String parseGeoIpResponse(Element response) { // TODO return null; } private Source toXmlSource(Document document) throws Exception { return new StreamSource(new StringReader($(document).toString())); } }
В этом примере я получаю небольшую помощь (и небольшую боль) от библиотеки JOOX для манипулирования XML в Java. Поскольку XML-библиотеки для Java безумны, я также отказываюсь от проверенных исключений.
Спойлер: Я вообще очень недоволен обработкой пространств имен, проверкой, XPath и проверенными исключениями во всех библиотеках XML, которые я нашел до сих пор. Поэтому я думаю о создании своего.
Конечно, вы можете использовать тот же подход с классами, которые автоматически генерируются из XSD, но я не уверен, что это действительно сильно поможет.
Поток XML через HTTP
Встроенный в Java HttpURLConnection — это неуклюжий, но полезный способ передачи XML на сервер (если только вы не выполняете расширенную HTTP-аутентификацию).
Тизер:
public class UrlSoapEndpoint { private final String url; public UrlSoapEndpoint(String url) { this.url = url; } public Document postRequest(String soapAction, Document soapRequest) throws Exception { URL httpUrl = new URL(url); HttpURLConnection connection = (HttpURLConnection) httpUrl.openConnection(); connection.setDoInput(true); connection.setDoOutput(true); connection.addRequestProperty("SOAPAction", soapAction); connection.addRequestProperty("Content-Type", "text/xml"); $(soapRequest).write(connection.getOutputStream()); int responseCode = connection.getResponseCode(); if (responseCode != 200) { throw new RuntimeException("Something went terribly wrong: " + connection.getResponseMessage()); } return $(connection.getInputStream()).document(); } }
Спойлер: этот код должен быть расширен регистрацией и обработкой ошибок, а проверка должна быть перенесена в декоратор. Взяв под контроль обработку HTTP, мы можем решить большую часть того, что решают люди, покупающие ESB.
Создать заглушку и разобрать XML
Заглушка использует xpath для поиска местоположения в запросе. Он генерирует ответ почти так же, как клиент ws генерировал запрос (не показан).
public class GeolocationServiceStub extends HttpServlet { private Map<String,String> locations = new HashMap<String, String>(); public void addLocation(String ipAddress, String country) { locations.put(ipAddress, country); } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { try { String ipAddress = $(req.getReader()).xpath("/Envelope/Body/GetGeoIP/IPAddress").text(); String location = locations.get(ipAddress); createResponse(location).write(resp.getOutputStream()); } catch (Exception e) { throw new RuntimeException("Exception at server " + e); } } }
Спойлер: Заглушки могут быть расширены, чтобы иметь веб-страницу, которая позволяет мне тестировать мою систему без реальной интеграции с каким-либо внешним сервисом.
Проверить и проанализировать ответ
Теперь клиент ws может проверить, что ответ от заглушки соответствует XSD, и проанализировать ответ. Опять же, это сделано с помощью XPath. Я не показываю код, так как он больше похож на тот же.
Настоящая вещь!
Теперь код проверяет, что полезная нагрузка XML соответствует XSD. Это означает, что клиент ws теперь должен использоваться с реальными вещами. Давайте напишем отдельный тест, чтобы проверить это:
public class GeolocationServiceLiveTest { @Test public void shouldFindLocation() throws Exception { GeolocationService wsClient = new GeolocationServiceWsClient("http://www.webservicex.net/geoipservice.asmx"); assertThat(wsClient.getCountryByIp("80.203.105.247")).isEqualTo("Norway"); } }
Ура! Оно работает! На самом деле, это не удалось с первого раза, так как у меня не было правильного названия страны для IP-адреса, с которым я тестировал.
Этот тип двухточечного интеграционного теста медленнее и менее надежен, чем другие мои модульные тесты. Тем не менее, я не нахожу ничего особенного из этого факта. Я отфильтровываю тест из моего конфига Infinitest, и меня это не волнует.
Уточнение всех услуг
SubscriptionRepository, CurrencyService и EmailService должны быть реализованы так же, как GeolocationService. Однако, поскольку мы знаем, что нам нужно только очень специфическое взаимодействие с каждой из этих служб, нам не нужно беспокоиться обо всем, что может быть отправлено или получено как часть служб SOAP. Пока мы можем выполнять работу, которая нужна бизнес-логике (CurrencyPublisher), мы готовы к работе!
Демонстрация и тестирование цепочки создания стоимости
Если мы создадим веб-интерфейс для заглушек, мы теперь сможем продемонстрировать всю цепочку создания стоимости этой услуги нашим клиентам. В моих проектах SOA некоторые сервисы, от которых мы зависим, будут доступны только в конце проекта. В этом случае мы можем использовать наши заглушки, чтобы показать, что наш сервис работает.
Спойлер: Поскольку я устал от проверки работы ручного теста цепочки создания ценности, я могу закончить тем, что создаю тест, который использует WebDriver для настройки заглушек и проверки того, что тест прошел нормально, так же, как и в ручном тесте.
Снятие перчаток во время боя на арене SOA
В этой статье я показал и намекнул на более чем полдюжины методов работы с тестами, http, xml и валидацией, которые не включают в себя фреймворки, ESB или генерацию кода. Подход дает программисту 100% контроль над своим местом в экосистеме SOA. У каждой из областей есть намного больше глубины, чтобы исследовать. Дайте мне знать, если вы хотите, чтобы это было изучено.
О, и я также хотел бы, чтобы идеи по улучшению веб-сервисов использовались, так как электронная почта с геолокационной валютой довольно обманчива.