Статьи

Тизер: SOA с голыми суставами

 

Я работаю над этой идеей, и я не знаю, нравится ли вам это, ребята. Я хотел бы узнать ваше мнение о том, стоит ли изучать это дальше.

Вот в чем дело: я столкнулся с командами, которые при работе с технологиями 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. У каждой из областей есть намного больше глубины, чтобы исследовать. Дайте мне знать, если вы хотите, чтобы это было изучено.

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