Случайны выбор дневника Раскрыть/свернуть полный список возможностей


Найдено 4899 сообщений
Cообщения с меткой

test - Самое интересное в блогах

Следующие 30  »
rss_rss_hh_new

Создаем заглушки сервисов для интеграционного тестирования на Apache Camel (с использованием Scala DSL)

Среда, 31 Августа 2016 г. 08:55 (ссылка)

image

Это третья статья об использовании Scala в тестировании.

Сегодня будут рассмотрены примеры использования Apache Camel для создания тестовых заглушек, а также компонентов информационной системы.

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

Для разовой проверки интеграции мы бы использовали простое Java или Scala приложение, сценарий Apache JMeter или SoapUI. Но нам нужна система, которая постоянно работает, отвечает на запросы и не требует действий со стороны тестировщика — запустил и забыл. Для решения такой задачи мы можем создать приложение, основанное на фреймворке Apache Сamel.





Рассмотрим 5 примеров:




  1. Чтение файлов в одной кодировке, запись в другой;

  2. Запрос к веб сервису по расписанию и сохранение сообщения в хранилище данных;

  3. Реализация веб-сервиса, который возвращает сообщение в зависимости от параметра GET запроса;

  4. Чтение сообщения из очереди и отправка сообщения в БД;

  5. Пример маршрутизации по содержимому файла.



Коротко опишем инструмены, которые используются для решения задачи.

Apache camel ( http://camel.apache.org/ ) — Java фреймворк, предназначенный для реализации обмена сообщениями между отдельными приложениями, подсистемами информационной системы. Реализует подход к разработке связующего программного обеспечения Enterprise Integration Patterns (EIP). Позволяет работать с файлами, БД, менеджерами очередей, веб-сервисами, и другими компонентами — их более 240 видов на странице проекта component

В приложениие Camel описываются так называемые endpoints — конечные точки, и правила преобразования и маршрутизации сообщений между ними.

Компонент Camel реализует конечную точку. Это либо производитель сообщения (Producer), либо потребитель(Consumer). Некоторые компоненты могут реализовывать оба вида точек, к примеру, из файла можно получить сообщени и записать. Некоторые компоненты реализуют только производителя сообщения, например таймер, или потребителя, например вывод в лог.

В ходе работы приложения происходит манипулирование телом сообщения и его заголовками.Порядок работы с Camel следующий:




  1. Описываем источник сообщения (файл, очередь, БД, сервис, таймер и т.п.);

  2. Описываем правила преобразования данных и форматов;

  3. Описываем получателя (получателей) сообщения(файл, очередь, БД, сервис, вывод в консоль и т.п.) и логику маршрутизации;

  4. Запускаем приложение, которое слушает источник, и при появлении сообщения преобразует его и маршрутизирует до получателей.



Для описания правил маршрутизации и преобразования сообщений используются различные языки languages. Для себя мы выбрали Scala DSL scala-dsl-eip, потому что этот язык хорошо подходит для простого и быстрого создания компонентного программного обеспечения. Для Scala используем систему сборки проекта SBT.

Cуществует отличный пример с чтением сообщения из файла и отправкой его http post запросом. Он немного устаревший, но может быть полезен.

http://www.lightbend.com/activator/template/camel-http

https://github.com/hilton/activator-camel-http#master



Подготовительные работы

Создадим проект в idea на основе SBT. Пример создания проекта можно подсмотреть — Реализация мониторинга и интеграционного тестирования информационной системы с использованием Scalatest. Часть1

В файле build.sbt пропишем настройки



name := "camel-scaladsl"
version := "1.0"
scalaVersion := "2.11.8"
val camelVersion = "2.17.1"

libraryDependencies ++= Seq(
// Компоненты для Camel
"org.apache.camel" % "camel-core" % camelVersion,
"org.apache.camel" % "camel-scala" % camelVersion,
// Для каждого компонента Camel своя зависимость
"org.apache.camel" % "camel-quartz" % camelVersion,
"org.apache.camel" % "camel-spring-redis" % camelVersion,
"org.apache.camel" % "camel-http" % camelVersion,
"org.apache.camel" % "camel-jetty" % camelVersion,
"org.apache.camel" % "camel-jms" % camelVersion,
"org.apache.camel" % "camel-jdbc" % camelVersion,
// Компоненты для логгирования
"ch.qos.logback" % "logback-classic" % "1.1.2",
"org.slf4j" % "slf4j-api" % "1.7.7",
// Компонент для работы xml в скала
"org.scala-lang.modules" % "scala-xml_2.11" % "1.0.5",
// Драйвер БД H2
"com.h2database" % "h2" % "1.4.192",
"org.apache.commons" % "commons-dbcp2" % "2.1.1",
// Драйвер для брокера activemq
"org.apache.activemq" % "activemq-client" % "5.13.3"
)


Добавим файл src/main/resources файл logback.xml, в котором настроен уровень логгирования и формат сообщения.






%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n







Иначе по-умолчанию будет уровень DEBUG — выводится слишком много информации.



Пример 1

Чтение файлов в одной кодировке, запись в другой.

Это простое приложение, которое использует компонент http://camel.apache.org/file2.html из пакета camel-core. Оно состоит из объекта, запускающего приложение FromFileToFileApp и класса FromFileToFileRoute, в котором описаны маршруты. Класс с маршрутами можно вынести в отдельный файл.



Содержимое файла src/main/scala/FromFileToFileApp.scala



import org.apache.camel.CamelContext
import org.apache.camel.main.Main
import org.apache.camel.scala.dsl.builder.{ScalaRouteBuilder, RouteBuilderSupport}

object FromFileToFileApp extends App with RouteBuilderSupport {
//Создаем Camel Main класс и контекст в нем
val mainApp = new Main
val context = mainApp.getOrCreateCamelContext
// Привязываем классы с маршрутами
mainApp.addRouteBuilder(new FromFileToFileRoute(context))
// Запускаем
mainApp.run
}

class FromFileToFileRoute(context: CamelContext) extends ScalaRouteBuilder(context) {
// Читаем содержимое файла в одной кодировке из папки "inbox"
"""file:inbox?charset=utf-8""" ==> {
// Пишем в другой кодировке в директорию "outbox"
to ("file:outbox?charset=Windows-1251")
}
}


В классе FromFileToFileRoute не происходит никаких преобразований с содержимым сообщения, отсутствует маршрутизация. После запуска приложения в папке проекта будут автоматически созданы папки "inbox", "outbox". При попадании в директорию "inbox", файл автоматически считывается — исчезает из папки. Затем он появляется в директории "outbox" в другой кодировке. При этом в папке "inbox" в отдельной подпапке будут храниться сообщения, прочитанные Сamel.



Пример 2

Запрос к веб-сервису по расписанию и сохранение сообщения в хранилище данных.

В этом примере по таймеру будем собирать даные о курсе валют и отправлять в Redis. Для того, чтобы выпонить действия над сообщением (записать тело и заголовки), существует метод "process". Для Redis отправка значений производится с помощью пары заголовков "CamelRedis.Key"/"CamelRedis.Value"

Нам необходимо извлечь тело сообщения, которое возвращает HTTP GET запрос и сделать его заголовком "CamelRedis.Value"

Ключ будем генерировать уникальный, подходящий для сортировки — текущее время в миллисекундах.



import org.apache.camel.{Exchange, CamelContext}
import org.apache.camel.main.Main
import org.apache.camel.scala.dsl.builder.{ScalaRouteBuilder, RouteBuilderSupport}
import org.springframework.data.redis.serializer.StringRedisSerializer

object FromHTTPToRedisApp extends App with RouteBuilderSupport{
val mainApp = new Main
// Прописываем вместо стандартного кастомный stringSerializer для Redis
mainApp.bind("stringSerializer",new StringRedisSerializer)
val context = mainApp.getOrCreateCamelContext
mainApp.addRouteBuilder(new FromHTTPToRedisRoute(context))
mainApp.run
}

class FromHTTPToRedisRoute (context: CamelContext) extends ScalaRouteBuilder(context) {
// По таймеру, раз в минуту обращаемся к HTTP сервису
"""quartz:timerName?cron=0+0/1+*+*+*+?""" ==> {

// Вывод информации в консоль
log("Запрос к сервису")
// Запрос к сервису
to("http://www.google.com/finance/info?q=CURRENCY%3aUSDRUB")
// Создание пары ключ-значение для Кedis, запись в заголовок
process((exchange: Exchange) => {
exchange.getOut.setHeader("CamelRedis.Key",System.currentTimeMillis())
exchange.getOut.setHeader("CamelRedis.Value",exchange.getIn.getBody(classOf[String]))
})
// Логгирование через отправку в конечную точку позволяет просмотреть сообщение и его атрибуты
// В данном случае тело сообщения будет пусто (Body: [Body is null]])
to("log:FromHTTPToRedisApp")
// Отправляем данные в Redis
// #stringSerializer - объявленный нами ранее кастомный сериалайзер
to("""spring-redis://172.16.7.58:6379?serializer=#stringSerializer""")
}
}


Чтобы писать в Redis с удаленного хоста, может понадобиться разрешение. К примеру, в консоли Redis на хосте, где он запущен, выполнить команду



CONFIG SET protected-mode no


Пример отображения записей в Redis представлен на рисунке.



image

Пример 3

Реализация веб-сервиса, который возвращает сообщение в зависимости от параметра GET запроса

В данном примере с помощью компонента Jetty реализуем простой HTTP сервер, который получает GET запрос с параметром и возвращает xml со значением параметра, либо с ошибкой.



object JettyApp extends App with RouteBuilderSupport{ 
val mainApp = new Main
val context = mainApp.getOrCreateCamelContext
mainApp.addRouteBuilder(new JettyRoute(context))
mainApp.run
}

class JettyRoute(context: CamelContext) extends ScalaRouteBuilder(context) {
// Определяем порт и адрес сервиса
"""jetty:http://0.0.0.0:1234/myapp/myservice""" ==> {
delay(2 seconds)
process((exchange: Exchange) => {
// Извлекаем значение параметра uuid из get запроса к сервису
val uuidParam = exchange.getIn.getHeader("uuid")
// Определяем паттерн для параметра
val pattern = """[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}""".r
// Возвращаем ответ в зависимости от извлеченного значения
// Возможен случай отсутсвия параметра, а также несоответствия значения шаблону uuid
def responseText = uuidParam match {
case null => "Uuid parameter not found"
case pattern() => s"$uuidParam"
case _ => s"Uuid parameter format is not valid"
}
// Определяем тип возвращаемого контента как xml
exchange.getOut().setHeader(Exchange.CONTENT_TYPE,"text/xml; charset=utf-8")
// Возвращаем xml с ответом.
exchange.getOut().setBody({responseText})
// Вариант отправки параметра как строки s"$responseText" тоже рабочий
})
}
}


Примеры запросов для проверки;

http://localhost:1234/myapp/myservice?uuid=2a577d52-e5a1-4da5-96e5-bdba1f68e6f1;

http://localhost:1234/myapp/myservice?uuid=123;

http://localhost:1234/myapp/myservice;

http://localhost:1234/myapp/myservice?guid=2a577d52-e5a1-4da5-96e5-bdba1f68e6f.



Примеры ответов сервиса представлены на рисунке



image

Пример 4

Чтение сообщения из очереди и запись в БД.

Работа с очередями и БД была выделена в отдельный пример. Настройка этих компонентов требует иного подхода. Если в предыдущих примерах настройка проводилась с помощью параметров в строке endpoint, то здесь нужно заранее создать объект, сделать на его основе компонент и использовать далее.

Для БД создаем экземпляр класса org.apache.commons.dbcp2.BasicDataSource и передаем ему параметры подключения. Для очереди создаем экземпляр класса javax.jms.ConnectionFactory, в котором также сохраняем параметры подключения. Далее для этих компонентов создается имя для конечной точки, и используется в URI. Разница в том, что для БД используется компонент "camel-jdbc", а для очередей создается новый компонент на основе "camel-jms"

Таблица, в которую происходит вставка записи в примерах, создается следующим запросом:



CREATE TABLE MESSAGETABLE(
ID UUID NOT NULL PRIMARY KEY,
DATETIME TIMESTAMP,
BODY VARCHAR(65536)


Следующий код будет забирать сообщения из очереди, выполнять в БД запрос на добавление уникального идентификатора, времени и тела сообщения.



import java.text.SimpleDateFormat
import java.util.{UUID, Date}
import org.apache.camel.component.jms.JmsComponent
import org.apache.camel.main.Main
import org.apache.camel.scala.dsl.builder.{RouteBuilderSupport, ScalaRouteBuilder}
import org.apache.camel.{CamelContext, Exchange}
// Для создания подключения к БД импортируем BasicDataSource
import org.apache.commons.dbcp2.BasicDataSource
// Для работы с месседж-брокером импортируем соответствующий ConnectionFactory класс
import org.apache.activemq.ActiveMQConnectionFactory

object FromMQToDBApp extends App with RouteBuilderSupport {
val mainApp = new Main
// Для работы с БД создаем объект и передаем ему свойства соединения
val ds = new BasicDataSource
ds.setDriverClassName("org.h2.Driver")
ds.setUrl("jdbc:h2:./h2db")
// Добавляем endpoint в приложение, далее в названии получателя будем использовать "h2db"
mainApp.bind("h2db",ds)
// Для работы с очередью создаем MQConnectionFactory
val cf = new ActiveMQConnectionFactory("tcp://192.168.3.38:61616")
// Создаем компонент для работы с очередью
mainApp.bind("amq-jms", JmsComponent.jmsComponentAutoAcknowledge(cf))
val context = mainApp.getOrCreateCamelContext
mainApp.addRouteBuilder(new FromMQToDBAppRoute(context))
mainApp.run
}

// Класс реализует чтение сообщения из очереди и запись его в БД
class FromMQToDBAppRoute(context: CamelContext) extends ScalaRouteBuilder(context) {
// Читаем сообщение из очереди. Компонент называется также, как мы его назвали ранее - "amq-jms", имя очереди передается как параметр
// Для каждого менеджера очередей необходимо создавать свой компонент
"""amq-jms:queue:TESTQ""" ==> {

process((exchange: Exchange) => {
// Генериуем uuid, дату/время
val uuid = UUID.randomUUID
val time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())
// Извлекаем тело сообщения
val messageBody = exchange.getIn.getBody(classOf[String])
// Формируем запрос с параметрами
exchange.getOut.setBody(s"INSERT INTO PUBLIC.MESSAGETABLE (ID, DATETIME, BODY) VALUES('$uuid', '$time', '$messageBody')")
})
// Отправляем подготовленный запрос в БД
// Компонент называется jdbc, далее указывается конкретный DataSource
to("jdbc:h2db")
}
}


Следует помнить о том, что при попытке записи в БД сообщения больше длины поля (у таблицы, созданной запросом ранее, длина поля — 65536 символов) – возникнет ошибка. Ее можно решить, обрезая тело до нужного размера, либо добавив errorHandler(deadLetterChannel("file:error")), который будет отправлять сообщения, приводящие к ошибкам, в папку «error».

В примере рассмотрено взаимодействие с базой данных H2. Для других БД нужно добавить соответствующую библиотеку в build.sbt, определить имя класса драйвера, URL. Могут понадобиться другие свойства подключения, к примеру, имя пользователя и пароль.

Пример описания реквизитов подключения для работы с Postgresql:

Добавление библиотеки в build.sbt



      libraryDependencies += "org.postgresql" % "postgresql" % "9.4.1207"


Реализация в классе:



    val ds = new BasicDataSource {
setDriverClassName("org.postgresql.Driver")
setUrl(conf.getString("jdbc:postgresql://myhost:5432/mydb"))
setUsername(conf.getString("myusername"))
setPassword(conf.getString("mypassword"))
}


С очередями несколько сложнее.

Для некоторых из менеджеров очередей, библиотеки не открыты для доступа в репозиториях. В этом случае, используются *.jar файлы, которые хранятся в папке lib проекта.

Для любого менеджера очередей нужно создать соответствующий объект типа connection factory.

К примеру, код, обеспечивающий взаимодействие с IBM Websphere MQ, будет таким:



    val cf = new MQQueueConnectionFactory {
setHostName("myhost")
setPort(1414)
setTransportType(1)
setQueueManager("myqmname")
setChannel("mychannel")
}


Для Oracle Weblogic Jms еще интереснее. Если создать очереди по иструкции [How to Create a Simple JMS Queue in Weblogic Server 11g](https://blogs.oracle.com/soaproactive/entry/how_to_create_a_simple), то объявление компонента будет таким:


  val env = new util.Hashtable[String, String]
env.put(javax.naming.Context.INITIAL_CONTEXT_FACTORY, "weblogic.jndi.WLInitialContextFactory")
env.put(javax.naming.Context.PROVIDER_URL, "t3://myhost:7001")
val ic: InitialContext = new InitialContext(env)
val connectionFactory = ic.lookup("jms/TestConnectionFactory").asInstanceOf[QueueConnectionFactory]
// где jms/TestConnectionFactory - jndi для ConnectionFactory"
mainApp.bind("ora-jms", JmsComponent.jmsComponentAutoAcknowledge(connectionFactory))


а endpoint URI будет такого формата: "ora-jms:queue:./TestJMSModule!TestJMSQueue", где ./ обозначает текущий сервер, "TestJMSModule" JNDI имя модуля "TestJMSQueue" — JNDI имя очереди



Пример 5

Маршрутизация по содержимому файла

В данном примерерассмотрим маршрутизацию сообщения в зависимости от его содержимого.

Предположим, что на входе имеется xml-сообщение, обработка которого зависит от значения элемента "То".

ActiveMQ — нужно отправить в очередь, а H2 — обработать каким-то образом и отправить в БД, someAdress — обработать еще каким-то образом.

В сообщение будет добавлен заголовок "Destination" с именем конечной точки, в которую надо будет отправить сообщение.

Если возникнет ошибка при обработке сообщения или в таблице маршрутизации не будет соответствующего значения, то отправляем сообщение в "direct:trash".

В примере используется конструкция скала "???", которая позволяет заменить несуществующий блок кода для успешной компиляции. Вместо этого блока нужно написать логику обработки.



import org.apache.camel.{Exchange, CamelContext}
import org.apache.camel.scala.dsl.builder.ScalaRouteBuilder
import scala.xml.XML

class ContentOrientedRouting(context: CamelContext) extends ScalaRouteBuilder(context) {

// При ошибках обработки сообщения, отправляем его в "direct:trash"
errorHandler(deadLetterChannel("direct:trash"))

// Опишем таблицу маршрутизации в виде Map
val destMap = Map(
"ActiveMQ" -> "jms-amq:queue:inbox",
"H2" -> "direct:h2db",
"someAdress" -> "direct:outbox")
// Вынесем обработку в отдельную функцию
val addRoutingAction = (exchange: Exchange) => {
// Получим значение тега "To" из XML-файла, который пришел на вход
val body = exchange.getIn.getBody(classOf[String])
val xmlBody = XML.loadString(body)
val toValue = (xmlBody \\ "To").text
// Получим имя endpoint, если такого значения нет - отправляем в "direct:trash"
val dest = destMap.getOrElse(toValue,"direct:trash")
// Устанавливаем значение заголовка
exchange.getOut.setHeader("Destination", dest)
}

"""direct:inbox1""" ==> {
process(addRoutingAction)
// извлекаем из заголовка "Destination" endpoint и отправляем туда сообщение
recipients(_.in("Destination"))
}
// Описываем логику для разных endpoint
"""jms-amq:queue:inbox""" ==> {???}

"""direct:h2db""" ==> {
process((exchange: Exchange) => {???})
to ("jdbc:h2db")
}

"""direct:outbox""" ==> {
// Параллельная отправка сообщения в файл и в лог
to("file:someFile", "log:Somelog")
}

"""direct:trash""" ==> {???}
}


Примеры показывают, как можно реализовать небольшое приложение для наших целей.

Рассмотрим дополнительные аспекты. позволяющие сделать разработку и обслуживание приложения удобнее.

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

в build.sbt добавляем



libraryDependencies += "com.typesafe" % "config" % "1.3.0"


в папке src/main/resources создаем файл application.conf, в котором прописываем настройки и вызываем их из кода.

Запуск приложения выполняется командой sbt run. В некоторых случаях это может быть неудобно.

Возможно создание jar файла с помощью плагина sbt-assembly https://github.com/sbt/sbt-assembly для запуска командой java –jar camelapp.jar. В *.jar файле будут содержаться все зависимости, поэтому размер будет большой, но запуск происходит сразу, без скачивания компонентов.

Для запуска в фоне удобно использовать приложение nohup.

Создаем скрипт для запуска в папке, которая входит в переменную среды $PATH, чтобы вызывать по имени из любой директории. К примеру в /usr/local/bin/. Скрипт для запуска:

/usr/local/bin/camelstart



#!/bin/bash
/usr/bin/nohup java -jar /opt/camelapp.jar&


Для остановки:

/usr/local/bin/camelstop



#!/bin/bash
pkill -f camelapp


Запуск приложения делается командой camelstart, остановка — camelstop.



Можем выделить некоторые плюсы и минусы использования Apache Camel.

Плюсы:




  • Быстрая реализация приложений;

  • Большое количество готовых компонентов;

  • Многопоточность, параллельная обработка сообщений из коробки;

  • Возможность выбора способа описания в виде XML или одного из DSL;



Минусы:




  • У каждого компонента своя логика работы, требуется время на понимание;

  • Существует порог входа;



Кроме того, поскольку Apache Camel работает на JVM, приложениям, созданным на его основе, присущи плюсы и минусы этой платформы.



Опыт использования Apache Camel в связке со ScalaDSL в нашей компании показал его эффективность для создания заглушек, компонентов интеграции а иногда и нагрузочных тестов.


Original source: habrahabr.ru (comments, light).

https://habrahabr.ru/post/306792/

Комментарии (0)КомментироватьВ цитатник или сообщество
alisa_davidsone

Без заголовка

Пятница, 17 Июня 2016 г. 18:44 (ссылка)


Получите свою карту
Другие тесты и гадания от Шувани
Метки:   Комментарии (0)КомментироватьВ цитатник или сообщество
alisa_davidsone

Без заголовка

Четверг, 09 Июня 2016 г. 11:29 (ссылка)



А какая дорога ваша?
Метки:   Комментарии (0)КомментироватьВ цитатник или сообщество

Следующие 30  »

<test - Самое интересное в блогах

Страницы: [1] 2 3 ..
.. 10

LiveInternet.Ru Ссылки: на главную|почта|знакомства|одноклассники|фото|открытки|тесты|чат
О проекте: помощь|контакты|разместить рекламу|версия для pda