На высоком уровне разные стратегии микросервиса имеют много общего. Они подписываются на одни и те же идеалы. Когда дело доходит до деталей того, как они на самом деле реализуются, они могут различаться.
Микросервисы в мире Chronicle созданы вокруг:
- Простота — просто, быстро, гибко и проще в обслуживании.
- Прозрачность — вы не можете контролировать то, что не понимаете.
- Воспроизводимость — это должно быть в вашем дизайне, чтобы обеспечить качественное решение.
Что мы подразумеваем под простым?
Ключевой частью дизайна микросервисов является то, как сообщения передаются между сервисами / компонентами. Простейшие сообщения можно назвать асинхронными вызовами методов .
Асинхронный вызов метода это тот, который;
- ничего не возвращает,
- не меняет своих аргументов;
- не генерирует никаких исключений (хотя основной транспорт может).
Причина, по которой этот подход используется, заключается в том, что самый простой транспорт — это вообще не транспорт. Один компонент вызывает другой. Это не только быстро (и с встраиванием может вообще не занимать время), но и просто в настройке, отладке и профилировании. Для большинства юнит-тестов вам не нужен реальный транспорт, поэтому нет смысла делать тест более сложным, чем это необходимо.
Давайте посмотрим на пример.
Скажем, у нас есть сервис / компонент, который постоянно обновляет рыночные данные. В простейшем случае это может быть обновление рынка только с одной стороны, покупка или продажа. Компонент может преобразовать это в полное обновление рынка, объединяющее цену покупки и продажи и количество.
В этом примере у нас есть только один тип сообщений, однако мы можем добавить больше типов сообщений. Я предлагаю вам создать другое имя / метод сообщения для каждого сообщения, а не перегружать метод.
Наша входящая структура данных
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
public class SidedPrice extends AbstractMarshallable { final String symbol; final long timestamp; final Side side; final double price, quantity; public SidedPrice(String symbol, long timestamp, Side side, double price, double quantity) { this .symbol = symbol; this .timestamp = timestamp; this .side = side; this .price = price; this .quantity = quantity; } } |
Наша исходящая структура данных
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
|
public class TopOfBookPrice extends AbstractMarshallable { final String symbol; final long timestamp; final double buyPrice, buyQuantity; final double sellPrice, sellQuantity; public TopOfBookPrice(String symbol, long timestamp, double buyPrice, double buyQuantity, double sellPrice, double sellQuantity) { this .symbol = symbol; this .timestamp = timestamp; this .buyPrice = buyPrice; this .buyQuantity = buyQuantity; this .sellPrice = sellPrice; this .sellQuantity = sellQuantity; } // more methods (1) } |
Компонент, который принимает односторонние цены, может иметь интерфейс;
Входящий интерфейс для первого компонента
1
2
3
|
public interface SidedMarketDataListener { void onSidedPrice(SidedPrice sidedPrice); } |
и его вывод также имеет один метод;
Исходящий интерфейс для первого компонента
1
2
3
|
public interface MarketDataListener { void onTopOfBookPrice(TopOfBookPrice price); } |
Как выглядит наш микросервис?
На высоком уровне объединитель очень прост;
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
public class SidedMarketDataCombiner implements SidedMarketDataListener { final MarketDataListener mdListener; final Map<String, TopOfBookPrice> priceMap = new TreeMap<>(); public SidedMarketDataCombiner(MarketDataListener mdListener) { this .mdListener = mdListener; } public void onSidedPrice(SidedPrice sidedPrice) { TopOfBookPrice price = priceMap.computeIfAbsent(sidedPrice.symbol, TopOfBookPrice:: new ); if (price.combine(sidedPrice)) mdListener.onTopOfBookPrice(price); } } |
Он реализует наш интерфейс ввода и принимает интерфейс вывода в качестве слушателя.
Что обеспечивает AbstractMarshallable?
Класс AbstractMarshallable является вспомогательным классом, который реализует toString()
, equals(Object)
и hashCode()
. Он также поддерживает записи MarshallableMarshallable writeMarshallable(WireOut)
и readMarshallable(WireIn)
.
Реализации по умолчанию используют все нестатические непереходные поля для печати, сравнения или построения hashCode.
результирующий toString () всегда можно
Marshallable.fromString(CharSequence)
с помощью Marshallable.fromString(CharSequence)
. Давайте посмотрим на пару примеров.
01
02
03
04
05
06
07
08
09
10
11
12
13
|
SidedPrice sp = new SidedPrice( "Symbol" , 123456789000L, Side.Buy, 1.2345 , 1_000_000); assertEquals( "!SidedPrice {\n" + " symbol: Symbol,\n" + " timestamp: 123456789000,\n" + " side: Buy,\n" + " price: 1.2345,\n" + " quantity: 1000000.0\n" + "}\n" , sp.toString()); // from string SidedPrice sp2 = Marshallable.fromString(sp.toString()); assertEquals(sp2, sp); assertEquals(sp2.hashCode(), sp.hashCode()); |
Как вы можете видеть, toString()
написан на YAML , лаконичен, читаем для человека и в коде.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
TopOfBookPrice tobp = new TopOfBookPrice( "Symbol" , 123456789000L, 1.2345 , 1_000_000, 1.235 , 2_000_000); assertEquals( "!TopOfBookPrice {\n" + " symbol: Symbol,\n" + " timestamp: 123456789000,\n" + " buyPrice: 1.2345,\n" + " buyQuantity: 1000000.0,\n" + " sellPrice: 1.235,\n" + " sellQuantity: 2000000.0\n" + "}\n" , tobp.toString()); // from string TopOfBookPrice topb2 = Marshallable.fromString(tobp.toString()); assertEquals(topb2, tobp); assertEquals(topb2.hashCode(), tobp.hashCode()); } |
Одним из преимуществ использования этого формата является то, что он облегчает поиск причины неудачного теста даже в сложных объектах.
Даже в тривиальном тесте не очевидно, в чем проблема
1
2
3
4
|
TopOfBookPrice tobp = new TopOfBookPrice( "Symbol" , 123456789000L, 1.2345 , 1_000_000, 1.235 , 2_000_000); TopOfBookPrice tobp2 = new TopOfBookPrice( "Symbol" , 123456789000L, 1.2345 , 1_000_000, 1.236 , 2_000_000); assertEquals(tobp, tobp2); |
Однако, когда вы запускаете этот тест в вашей IDE , вы получаете окно сравнения.
Рисунок 1. Сравнение Windows в вашей IDE
Если у вас есть большой вложенный / сложный объект, где assertEquals
терпит неудачу, это действительно может сэкономить вам много времени, чтобы найти, какова дискретность.
Издеваться над нашим компонентом
Мы можем смоделировать интерфейс, используя такой инструмент, как EasyMock . Я считаю, что EasyMock проще при работе с интерфейсами, управляемыми событиями. Он не такой мощный, как PowerMock или Mockito, однако, если вы делаете все просто, вам могут не понадобиться эти функции.
01
02
03
04
05
06
07
08
09
10
11
|
// what we expect to happen SidedPrice sp = new SidedPrice( "Symbol" , 123456789000L, Side.Buy, 1.2345 , 1_000_000); SidedMarketDataListener listener = createMock(SidedMarketDataListener. class ); listener.onSidedPrice(sp); replay(listener); // what happens listener.onSidedPrice(sp); // verify we got everything we expected. verify(listener); |
Мы также можем высмеивать ожидаемый результат компонента тем же способом.
Тестирование нашего компонента
Посмеиваясь над выходным интерфейсом и вызывая входной интерфейс для нашего компонента, мы можем проверить, работает ли он должным образом.
01
02
03
04
05
06
07
08
09
10
|
MarketDataListener listener = createMock(MarketDataListener. class ); listener.onTopOfBookPrice( new TopOfBookPrice( "EURUSD" , 123456789000L, 1.1167 , 1_000_000, Double.NaN, 0 )); ( 1 ) listener.onTopOfBookPrice( new TopOfBookPrice( "EURUSD" , 123456789100L, 1.1167 , 1_000_000, 1.1172 , 2_000_000)); ( 2 ) replay(listener); SidedMarketDataListener combiner = new SidedMarketDataCombiner(listener); combiner.onSidedPrice( new SidedPrice( "EURUSD" , 123456789000L, Side.Buy, 1.1167 , 1e6)); ( 1 ) combiner.onSidedPrice( new SidedPrice( "EURUSD" , 123456789100L, Side.Sell, 1.1172 , 2e6)); ( 2 ) verify(listener); |
1
2
|
Setting the buy price Setting the sell price |
Тестирование серии компонентов
Давайте добавим OrderManager в качестве компонента нисходящего потока. Этот менеджер заказов будет получать как рыночные данные, так и идеи заказов, и в свою очередь будет создавать заказы.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
|
// what we expect to happen OrderListener listener = createMock(OrderListener. class ); listener.onOrder( new Order( "EURUSD" , Side.Buy, 1.1167 , 1_000_000)); replay(listener); // build our scenario OrderManager orderManager = new OrderManager(listener); ( 2 ) SidedMarketDataCombiner combiner = new SidedMarketDataCombiner(orderManager); ( 1 ) // events in orderManager.onOrderIdea( new OrderIdea( "EURUSD" , Side.Buy, 1.1180 , 2e6)); // not expected to trigger combiner.onSidedPrice( new SidedPrice( "EURUSD" , 123456789000L, Side.Sell, 1.1172 , 2e6)); combiner.onSidedPrice( new SidedPrice( "EURUSD" , 123456789100L, Side.Buy, 1.1160 , 2e6)); combiner.onSidedPrice( new SidedPrice( "EURUSD" , 123456789100L, Side.Buy, 1.1167 , 2e6)); orderManager.onOrderIdea( new OrderIdea( "EURUSD" , Side.Buy, 1.1165 , 1e6)); // expected to trigger verify(listener); |
1
2
|
The first component combines sided prices The second component listens to order ideas and top of book market data |
Отладка наших компонентов
Вы можете видеть, что один компонент просто вызывает другой. При отладке этого однопоточного кода каждое событие первого компонента является вызовом второго компонента. Когда это заканчивается, оно возвращается к первому и тестам.
Когда какое-либо отдельное событие вызывает ошибку, вы можете увидеть в трассировке стека, какое событие вызвало проблему. Однако, если вы ожидаете событие, которое не произойдет, это будет сложно, если ваши тесты не просты (или вы делаете серию простых тестов с помощью verify()
, reset()
и replay()
.
запуск теста и его отладка в вашей среде IDE практически не занимает времени. Вы можете запустить сотни таких тестов менее чем за секунду.
Источник для примеров
Как мы создаем их как сервисы?
Мы показали, как легко тестировать и отлаживать наши компоненты. Как мы превращаем их в услуги в части 2 .
Ссылка: | Микросервисы в мире хроники — часть 1 от нашего партнера по JCG Питера Лоури из блога Vanilla Java . |