Статьи

Скажите нам, что вы хотите, и мы сделаем это так: потребительское контрактное тестирование для обмена сообщениями

Некоторое время назад мы говорили о контрактном тестировании, ориентированном на потребителя, с точки зрения веб-API REST (ful) в целом и их проекции на Java ( спецификация JAX-RS 2.0 ) в частности. Было бы справедливо сказать, что REST по- прежнему доминирует в ландшафте веб-API, по крайней мере, в отношении общедоступных API, однако сдвиг в сторону микросервисов и / или архитектуры на основе сервисов очень быстро меняет расстановку сил. Одна из таких разрушительных тенденций — обмен сообщениями .

Современные REST (ful) API реализуются в основном по протоколу HTTP 1.1 и ограничены его стилем связи запрос / ответ. HTTP / 2 здесь, чтобы помочь, но, тем не менее, не все варианты использования вписываются в эту модель связи. Зачастую работа может выполняться асинхронно, а факт ее завершения может быть передан заинтересованным сторонам позднее. Вот как большинство вещей работает в реальной жизни, и использование обмена сообщениями является идеальным ответом на это.

Пространство обмена сообщениями действительно переполнено удивительным количеством брокеров сообщений и доступных вариантов без посредников. Мы не будем говорить об этом, а сосредоточимся на другой хитрой теме: сообщение сжимается. Как только производитель отправляет сообщение или событие, оно попадает в очередь / тему / канал, готовое к использованию. Это здесь, чтобы остаться на некоторое время. Очевидно, производитель знает, что он публикует, но как быть с потребителями? Как они узнают, чего ожидать?

В этот момент многие из нас будут кричать: использовать сериализацию на основе схемы! И действительно, Apache Avro , Apache Thrift , Protocol Buffers , Message Pack … находятся здесь для решения этой проблемы. В конце концов, такие сообщения и события становятся частью контракта с поставщиком вместе с веб-API REST (ful), если таковые имеются, и должны передаваться и развиваться с течением времени, не нарушая потребителей. Но … вы были бы удивлены, узнав, сколько организаций нашли свою нирвану в JSON и используют ее для передачи сообщений и событий, бросая такие клочья в потребителей, никакой схемы вообще! В этом посте мы рассмотрим, как методика контрактного тестирования, ориентированная на потребителя, может помочь нам в такой ситуации.

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

Поскольку служба заказа реализована в Java, события представляют собой просто классы POJO, сериализованные в JSON, прежде чем поступить в брокер сообщений с использованием одной из многочисленных библиотек. OrderConfirmed является одним из таких событий.

01
02
03
04
05
06
07
08
09
10
public class OrderConfirmed {
    private UUID orderId;
    private UUID paymentId;
    private BigDecimal amount;
    private String street;
    private String city;
    private String state;
    private String zip;
    private String country;
}

Как это часто бывает, команда службы отгрузки получила образец фрагмента JSON или указала на какой-то фрагмент документации или ссылочный класс Java, и это в основном так. Как команда службы отгрузки могла запустить интеграцию, будучи уверенной, что их интерпретация верна и данные сообщения, которые им нужны, не исчезнут внезапно? Потребительские контрактные испытания на помощь!

Команда службы отгрузки может (и должна) начать с написания контрольных примеров для сообщения OrderConfirmed , встраивая имеющиеся у них знания, и наш старый фреймворк Pact (точнее, Pact JVM ) является подходящим инструментом для этого. Так как же может выглядеть контрольный пример?

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class OrderConfirmedConsumerTest {
    private static final String PROVIDER_ID = "Order Service";
    private static final String CONSUMER_ID = "Shipment Service";
     
    @Rule
    public MessagePactProviderRule provider = new MessagePactProviderRule(this);
    private byte[] message;
 
    @Pact(provider = PROVIDER_ID, consumer = CONSUMER_ID)
    public MessagePact pact(MessagePactBuilder builder) {
        return builder
            .given("default")
            .expectsToReceive("an Order confirmation message")
            .withMetadata(Map.of("Content-Type", "application/json"))
            .withContent(new PactDslJsonBody()
                .uuid("orderId")
                .uuid("paymentId")
                .decimalType("amount")
                .stringType("street")
                .stringType("city")
                .stringType("state")
                .stringType("zip")
                .stringType("country"))
            .toPact();
    }
 
    @Test
    @PactVerification(PROVIDER_ID)
    public void test() throws Exception {
        Assert.assertNotNull(message);
    }
 
    public void setMessage(byte[] messageContents) {
        message = messageContents;
    }
}

Это исключительно просто и понятно, без добавления шаблона. Тестовый пример разработан прямо из JSON- представления сообщения OrderConfirmed . Но мы только на полпути, команда службы отгрузки должна каким-то образом передать свои ожидания обратно в службу заказов, чтобы производитель отслеживал, кто и как использует сообщение OrderConfirmed . Тестовая система Pact позаботится об этом, создав файлы pact (набор соглашений или пактов) из каждого тестового примера JUnit в папку ‘target / pacts’ . Ниже приведен пример сгенерированного файла договора Shipment Service-Order Service.json после запуска набора тестов OrderConfirmedConsumerTest .

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
{
  "consumer": {
    "name": "Shipment Service"
  },
  "provider": {
    "name": "Order Service"
  },
  "messages": [
    {
      "description": "an Order confirmation message",
      "metaData": {
        "contentType": "application/json"
      },
      "contents": {
        "zip": "string",
        "country": "string",
        "amount": 100,
        "orderId": "e2490de5-5bd3-43d5-b7c4-526e33f71304",
        "city": "string",
        "paymentId": "e2490de5-5bd3-43d5-b7c4-526e33f71304",
        "street": "string",
        "state": "string"
      },
      "providerStates": [
        {
          "name": "default"
        }
      ],
      "matchingRules": {
        "body": {
          "$.orderId": {
            "matchers": [
              {
                "match": "regex",
                "regex": "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"
              }
            ],
            "combine": "AND"
          },
          "$.paymentId": {
            "matchers": [
              {
                "match": "regex",
                "regex": "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"
              }
            ],
            "combine": "AND"
          },
          "$.amount": {
            "matchers": [
              {
                "match": "decimal"
              }
            ],
            "combine": "AND"
          },
          "$.street": {
            "matchers": [
              {
                "match": "type"
              }
            ],
            "combine": "AND"
          },
          "$.city": {
            "matchers": [
              {
                "match": "type"
              }
            ],
            "combine": "AND"
          },
          "$.state": {
            "matchers": [
              {
                "match": "type"
              }
            ],
            "combine": "AND"
          },
          "$.zip": {
            "matchers": [
              {
                "match": "type"
              }
            ],
            "combine": "AND"
          },
          "$.country": {
            "matchers": [
              {
                "match": "type"
              }
            ],
            "combine": "AND"
          }
        }
      }
    }
  ],
  "metadata": {
    "pactSpecification": {
      "version": "3.0.0"
    },
    "pact-jvm": {
      "version": "4.0.2"
    }
  }
}

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@RunWith(PactRunner.class)
@Provider(OrderServicePactsTest.PROVIDER_ID)
@PactFolder("pacts")
public class OrderServicePactsTest {
    public static final String PROVIDER_ID = "Order Service";
 
    @TestTarget
    public final Target target = new AmqpTarget();
    private ObjectMapper objectMapper;
     
    @Before
    public void setUp() {
        objectMapper = new ObjectMapper();
    }
 
    @State("default")
    public void toDefaultState() {
    }
     
    @PactVerifyProvider("an Order confirmation message")
    public String verifyOrderConfirmed() throws JsonProcessingException {
        final OrderConfirmed order = new OrderConfirmed();
         
        order.setOrderId(UUID.randomUUID());
        order.setPaymentId(UUID.randomUUID());
        order.setAmount(new BigDecimal("102.33"));
        order.setStreet("1203 Westmisnter Blvrd");
        order.setCity("Westminster");
        order.setCountry("USA");
        order.setState("MI");
        order.setZip("92239");
 
        return objectMapper.writeValueAsString(order);
    }
}

Жгут тестов выбирает все файлы pact из @PactFolder и запускает тесты для @TestTarget , в этом случае мы подключаем AmqpTarget , поставляемый из коробки, но вы можете легко подключить свою собственную конкретную цель.

И это в основном это! Потребители ( служба отгрузки ) выражают свои ожидания в тестовых примерах и делятся с производителем ( служба заказа ) в форме файлов пакта. Производители имеют собственный набор тестов, чтобы убедиться, что его модель соответствует мнению потребителей. Обе стороны могли бы продолжать развиваться независимо и доверять друг другу, поскольку пакты не денонсированы (надеюсь, никогда).

Чтобы быть справедливым, Pact — не единственный выбор для проведения контрактного тестирования , ориентированного на потребителя , в следующей публикации (уже в работе) мы поговорим о еще одном отличном варианте, Spring Cloud Contract .

На сегодняшний день полные исходники проекта доступны на Github .

Опубликовано на Java Code Geeks с разрешения Андрея Редько, партнера нашей программы JCG . Посмотрите оригинальную статью здесь: Скажите нам, что вы хотите, и мы сделаем это так: потребительское контрактное тестирование для обмена сообщениями

Мнения, высказанные участниками Java Code Geeks, являются их собственными.