Клятва на крови: контрактные тесты с Pact в .NET. Часть вторая

от автора

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

На примере RabbitMq определим список возможных составляющих контракта. В зависимости от конкретного инструмента список может сильно варьироваться:

  • Модель сообщения (структура, типы данных, формат)

  • Название, тип обменника

  • Название очереди

  • Настройки вроде routing key, topics, reply-to и т.д.

Материалы для демо

  • .NET, библиотека Pact поддерживает .netstandart2.0, демо использует .NET 6;

  • PactNet 5.0.0-beta.2 и PactNet.Abstractions 5.0.0-beta.2 для написания тестов; Причина использования предрелизной версии в том, что последний стабильный релиз библиотеки версии 4.5.0 не поддерживает non-ASCII символы. Также, до 5.x.x версии в качестве сериализатора по умолчанию использовался Newthonsoft.Json вместо более современного System.Text.Json;

  • библиотека EasyNetQ 7.8.0 и EasyNetQ.Serialization.SystemTextJson 7.8.0 для работы с RabbitMq;

  • Докер для запуска контейнеров RabbitMq и PactBroker.

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

Асинхронное взаимодействие между сервисами

Для тестирования сценариев с использованием RabbitMq добавим к существующей функциональности уведомление о готовности карты. Так, поставщик контракта (Demo.Provider) отправит в очередь модель с признаком необходимости отправки уведомления клиенту. В свою очередь потребитель (Demo.Consumer) обработает сообщение и, в зависимости от значения поля ShouldBeNotified, выведет на консоль сообщение, имитирующее уведомление пользователю.

Представим, что в результате согласования контракта были зафиксированы следующие договоренности:

  • Модель сообщения содержит поля описанные в классе CardOrderSatisfiedEvent и включает: код карты-продукта, идентификатор пользователя и признак необходимости отправки уведомления;

  • Название обменника SpecialExchangeName, тип direct;

  • Routing-key имеет значение super-routing-key.

Для реализации данного сценария добавим в сборки Consumer.Host и Provider.Host следующие зависимости:

 <PackageReference Include="EasyNetQ" Version="7.8.0" />  <PackageReference Include="EasyNetQ.Serialization.SystemTextJson" Version="7.8.0" />

С целью упрощения реализуем отправку сообщения непосредственно в контроллере сервиса Demo.Provider:

[HttpPost("order-satisfied/{userId}")] public async Task<ActionResult> SendCardOrderSatisfiedEvent(string userId) {     var advancedBus = RabbitHutch.CreateBus("host=localhost", s =>     {         s.EnableConsoleLogger();         s.EnableSystemTextJson();     }).Advanced;     var exchange = await advancedBus                         .ExchangeDeclareAsync("SpecialExchangeName", "direct");     var message = new Message<CardOrderSatisfiedEvent>(           new CardOrderSatisfiedEvent           {             UserId = userId,             CardCode = Random.Shared.Next(100)           });     await advancedBus.PublishAsync(exchange, "super-routing-key", false, message);     return Ok(); } 

В свою очередь подписку на событие со стороны потребителя реализуем прямо в классе Program, добавим где-нибудь следующий код:

var advanced = RabbitHutch.CreateBus("host=localhost:5672;username=guest;password=guest",    s =>       {         s.EnableConsoleLogger();         s.EnableSystemTextJson();         s.Register<ITypeNameSerializer, SimpleTypeNameSerializer>();       }).Advanced; var exchange = advanced.ExchangeDeclare("SpecialExchangeName", "direct"); var queue = advanced.QueueDeclare("SpecialQueueName"); advanced.Bind(exchange, queue, routingKey: "super-routing-key"); advanced.Consume<CardOrderSatisfiedEvent>(queue, (message, _) =>     Task.Factory.StartNew(() =>     {         var handler = app.Services.GetRequiredService<ConsumerCardService>();         if(message.Body.ShouldBeNotified)             handler.PushUser(message.Body);     }));  // BAD CODE, только для демо class SimpleTypeNameSerializer : ITypeNameSerializer {     public string Serialize(Type type) => type.Name;     public Type DeSerialize(string typeName) => typeof(CardOrderSatisfiedEvent); }   

Обычно при работе с EasyNetQ поставщик контракта создает отдельную nuget-сборку с необходимой моделью сообщения, поскольку по умолчанию для сериализации и десериализации используется свойство сообщения messageType. В рассматриваемом демо отсутствуют nugetсборки, проблема несоответствия Type.FullName двух моделей CardOrderSatisfiedEvent в разных проектах решается с помощью класса SimpleTypeNameSerializer, который переопределяет поведение десериализации. Просто добавлять ссылку на сборку с контрактом бессмысленно: мы не сможем имитировать «развитие» контракта и нарушить пакт.

Остается запустить в докере RabbitMq и проверить, что сервисы общаются с его использованием:

docker run --rm -d -p 15671:15671/tcp -p 15672:15672/tcp -p 25672:25672/tcp  -p 4369:4369/tcp -p 5671:5671/tcp -p 5672:5672/tcp rabbitmq:3-management

Тестирование на стороне потребителя

Для начала определимся с понятиями consumer / provider и subscriber / publisher, поскольку терминология здесь немного не очевидна и может запутать. Как было сказано в первой части, в терминах Pact consumer`ом считается клиент, потребитель API. Также под этим понятием понимается подписчик, получатель события или subscriber в терминах брокеров сообщений. Несмотря на то, что полезную работу над данными выполняет subscriber, в асинхронных системах поставщиком является publisher. Из этого следует, что в нашей демонстрации, сервис Demo.Provider является отправителем сообщения (publisher) и источником события (provider), а сервис Demo.Consumer служит получателем сообщения (subscriber) и потребителем события (consumer).

Создадим в папке Consumer.ContractTests/RabbitMq класc CardOrderSatisfiedEventTests и наполним его следующим содержимым:

Код класса CardOrderSatisfiedEventTests
public class CardOrderSatisfiedEventTests {     private readonly IMessagePactBuilderV4 _pactBuilder;     private const string ComType = "RABBITMQ";      public CardOrderSatisfiedEventTests(ITestOutputHelper testOutputHelper)     {         var pact = Pact.V4(consumer: "Demo.Consumer", provider: "Demo.Provider", new PactConfig         {             Outputters = new[] {new PactXUnitOutput(testOutputHelper)},             DefaultJsonSettings = new JsonSerializerOptions             {                 PropertyNameCaseInsensitive = true,                 PropertyNamingPolicy = JsonNamingPolicy.CamelCase             }         });         _pactBuilder = pact.WithMessageInteractions();     }      [Fact(DisplayName = "Demo.Provider присылает корректный контракт и пуш отправляется, " +                         "когда получено событие и необходимо уведомление клиента")]     public void CardOrderSatisfiedEvent_WhenModelCorrectAndShouldBePushed_SendsPush()     {         // Arrange         var message = new         {             UserId = Match.Type("rabbitmqUserId"),             CardCode = Match.Integer(100),             ShouldBeNotified = true         };          _pactBuilder             .ExpectsToReceive($"{ComType}: CardOrderSatisfiedEvent with push")             .WithMetadata("exchangeName", "SpecialExchangeName")             .WithMetadata("routingKey", "super-routing-key")             .WithJsonContent(message)              // Act             .Verify<CardOrderSatisfiedEvent>(msg =>             {                 // Assert                 // место для вызова IConsumer.Handle и проверки логики работы обработчика                 //_consumerCardService.Verify(x => x.PushUser(msg), Times.Once);             });     }          [Fact(DisplayName = "Demo.Provider присылает корректный контракт и пуш не отправляется, " +                         "когда получено событие и не нужно уведомление клиента")]     public void CardOrderSatisfiedEvent_WhenModelCorrectAndShouldNotBePushed_DontSendPush()     {         // Arrange         var message = new         {             UserId = Match.Type(string.Empty),             CardCode = Match.Integer(100),             ShouldBeNotified = false         };          _pactBuilder             .ExpectsToReceive($"{ComType}: CardOrderSatisfiedEvent no push")             .WithMetadata("exchangeName", "SpecialExchangeName")             .WithMetadata("routingKey", "super-routing-key")             .WithJsonContent(message)              // Act             .Verify<CardOrderSatisfiedEvent>(msg =>             {                 // Assert                 // место для вызова IConsumer.Handle и проверки логики работы обработчика                 //_consumerCardService.Verify(x => x.PushUser(msg), Times.Never);             });     } } 

  • Вместо IPactBuilderV4 используется IMessagePactBuilderV4, определяющий пакты для систем, взаимодействующих с помощью брокеров сообщений. Создается объект путем вызова метода WithMessageInteractions(). Остальной код конфигурации не отличается от части, использованной в тестах для HTTP;

  • Метод ExpectsToReceive() по аналогии с UponReceiving() определяет название теста, а знакомый метод WithJsonContent() определяет структуру и содержимое модели события. В то же время вызов метода WithMetadata() позволяет зафиксировать другие артефакты сообщения вроде заголовков, свойств и прочих настроек. В нашем случае тест ожидает от отправителя сообщений использования обменника с названием SpecialExchangeName и топика super-routing-key;

  • Синхронный Verify все также отвечает за формирование файла pact.json, но, в отличие от HTTP версии, здесь не нужно поднимать сервер, а в секции Assert можно проверить работу обработчика сообщения.

В целом, в рамках тестирования event-driven систем, Pact абстрагируется от понятия брокеров сообщений и не подразумевает реального асинхронного взаимодействия во время тестирования. Основное внимание уделяется соответствию модели события и отчасти её валидации. Ввиду такого обобщения брокеров, Pact не предоставляет конкретных методов для работы с каждым из них и может предложить только метод WithMetadata().

Так как значения для сущностей вроде названий обменников и топиков чаще всего хранятся только там, где непосредственно используются, проверка на их соответствие контракту усложняется. Дублирование их значений в тесте (как в нашем примере) скорее всего будет начисто забыто, чтение из IConfiguration потребует дополнительных усилий, а зависимость значений от среды окружения (dev, test, prod) лишь усугубляет всю эту ситуацию. Поэтому выстроить доверие к тесту, проверяющему данные артефакты, довольно сложно.

Тестирование на стороне поставщика

Теперь создадим в папке Provider.ContractTests/RabbitMq класc ContractWithConsumerTests и наполним его следующим содержимым:

Код класса ContractWithConsumerTests
public class ContractWithConsumerTests : IDisposable {     private readonly PactVerifier _pactVerifier;     private const string ComType = "RABBITMQ";      private readonly JsonSerializerOptions _jsonSerializerOptions = new()     {         PropertyNameCaseInsensitive = true,         PropertyNamingPolicy = JsonNamingPolicy.CamelCase     };      public ContractWithConsumerTests(ITestOutputHelper testOutputHelper)      {         _pactVerifier = new PactVerifier("Demo.Provider", new PactVerifierConfig         {             Outputters = new []{ new PactXUnitOutput(testOutputHelper) }         });     }          [Fact(DisplayName = "RabbitMq контракты с потребителем Demo.Consumer соблюдаются")]     public void Verify_RabbitMqDemoConsumerContacts()     {         // Arrange         var userId = "rabbitUserId";         var cardCode = 100;         var metadata = new Dictionary<string, string>         {             {"exchangeName", "SpecialExchangeName"},             {"routingKey", "super-routing-key"}         };         _pactVerifier.WithMessages(scenarios =>             {                 scenarios.Add($"{ComType}: CardOrderSatisfiedEvent with push", builder =>                 {                     builder.WithMetadata(metadata).WithContent(() => new CardOrderSatisfiedEvent                     {                         UserId = userId, CardCode = cardCode, ShouldBeNotified = true                     });                 });                 scenarios.Add($"{ComType}: CardOrderSatisfiedEvent no push", builder =>                 {                     builder.WithMetadata(metadata).WithContent(() => new CardOrderSatisfiedEvent                     {                         UserId = userId, CardCode = cardCode, ShouldBeNotified = false                     });                 });             }, _jsonSerializerOptions)             .WithFileSource(new FileInfo(@"..\..\..\pacts\Demo.Consumer-Demo.Provider.json"))                          // Act && Assert             .WithFilter(ComType)             .Verify();     }      public void Dispose()     {         _pactVerifier?.Dispose();     } }           

Вместо вызываемого раньше WithHttpEndpoint(), который использовал запущенное нами рядом приложение, метод WithMessages() выбирает первый свободный порт и отвечает за поднятие мок-хоста по адресу http://localhost:port/pact-messages/. Также данный метод принимает набор сценариев, каждый из которых включает в себя название, метаданные и тело сообщения. Такое решение обусловлено отсутствием реального брокера сообщений в рамках тестирования с Pact. Мы просто создаем абстракцию в виде MessageProvider и заполняем его нашими событиями. При выполнении теста этот виртуальный брокер сверит хранящиеся в нём сообщения с входными моделями из файла pact.json и выдаст результат при вызове метода Verify(). Также, поскольку теперь в файле с пактами содержатся как синхронные, так и асинхронные взаимодействия, вызов метода WithFilter() позволяет проверить только последние.

Смотрим pact.json и снова ломаем API

В результате прогона теста на стороне Demo.Provider в уже известный нам файл будет добавлено еще два взаимодействия, структура которых в общем похожа на предыдущие примеры. К основным отличиям можно отнести разве что определяемое уже нами содержимое секции metadata, а также иной тип взаимодействия в секции type.

{   "contents": {     "content": {       "cardCode": 100,       "shouldBeNotified": true,       "userId": "rabbitmqUserId"     },     "contentType": "application/json",     "encoded": false   },   "description": "RABBITMQ: CardOrderSatisfiedEvent with push",   "matchingRules": {     "body": {       "$.cardCode": {         "combine": "AND",         "matchers": [{"match": "integer"}]       },       "$.userId": {         "combine": "AND",         "matchers": [{"match": "type"}]       }    } },   "metadata": {     "exchangeName": "SpecialExchangeName",     "routingKey": "super-routing-key" },     "pending": false,     "type": "Asynchronous/Messages" },  {"description": "RABBITMQ: CardOrderSatisfiedEvent no push"...} 

Особых отличий в поведении от HTTP теста нет и при внесении несогласованных изменений в контракт. Так, при каком-либо изменении модели или метаданных Pact выдаст ошибку, вроде следующей:

Failures:  1) Verifying a pact between Demo.Consumer and Demo.Provider - RABBITMQ: CardOrderSatisfiedEvent with push     1.1) has a matching body            $.userId -> Expected 'rabbitmqUserId' (String) to be equal to 'diffUserId' (String)            $ -> Actual map is missing the following keys: cardCode      1.2) has matching metadata            Expected message metadata 'routingKey' to have value '"super-routing-key"' but was '"diff-super-routing-key"'            Expected message metadata 'exchangeName' to have value '"SpecialExchangeName"' but was '"DiffSpecialExchangeName"' 

Знакомимся с PactBroker

Исходя из всего вышесказанного сделанного и материала первой части, к данному моменту у нас есть два сервиса с контрактными тестами для каждого из них, покрывающих как взаимодействия по HTTP, так и полагающихся на RabbitMq. Тем не менее, мы все еще копируем файл Demo.Consumer-Demo.Provider.json из проекта в проект, что не очень удобно.

К счастью, роль доставщика пактов на себя может взять уже готовое приложение — PactBroker. Как становится понятно из названия, основная цель использования данного инструмента это автоматизированная доставка файла pact.json, однако, в том числе он предоставляет довольно информативную панель для просмотра существующих пактов.

Для работы PactBroker`у необходима база данных для хранения существующих контрактов. В официальной документации представлена вся информация о вариантах запуска PactBroker, мы же поднимем экземпляр с помощью docker-compose, представленного ниже. Файл описывает запуск кластера СУБД PostgreSQL 15, а также зависящего от него экземпляра брокера.

Код docker-compose.yaml
version: "3.9"  services:   postgres:     image: postgres:15     container_name: pact-postgres     ports:       - "5432:5432"     healthcheck:       test: psql postgres -U postgres --command 'SELECT 1'     environment:       POSTGRES_USER: postgres       POSTGRES_PASSWORD: postgres       POSTGRES_DB: postgres      broker:     image: pactfoundation/pact-broker:latest-multi     container_name: pact-broker-1     depends_on:       - postgres     ports:       - "9292:9292"     restart: always     environment:       PACT_BROKER_ALLOW_PUBLIC_READ: "false"       PACT_BROKER_BASIC_AUTH_USERNAME: admin       PACT_BROKER_BASIC_AUTH_PASSWORD: pass       PACT_BROKER_DATABASE_URL: "postgres://postgres:postgres@postgres/postgres"      healthcheck:       test: ["CMD", "curl", "--silent", "--show-error", "--fail",              "http://pactbroker:pactbroker@localhost:9292/diagnostic/status/heartbeat"]       interval: 1s       timeout: 2s       retries: 5 

В результате исполнения данного файла запускаются приложения базы данных и брокера.

Запущенные Postgress и PactBroker

Запущенные Postgress и PactBroker

Прикручиваем автоматизированную доставка пактов

Сохранение сгенерированных пактов в PactBroker

Несмотря на то, что библиотека PactNet предоставляет возможность получать из брокера пакты (что мы увидим совсем скоро), способность отправлять их в него она утратила. Субъективно, правильным решением в среде для реального приложения является отдельный шаг отправки сгенерированных пактов используя pact-cli. Но так как обзор pact-cli выходит за рамки данного материала, в нашем демо мы используем довольно противоречивое, однако более понятное для целей демонстрации решение.

Создадим в папке shared новый проект библиотеки классов. В нашем случае сгенерированные файлы будут отправляется брокеру в конце работы всех тестов класса. Для достижения этой цели используем интерфейс IClassFixture и метод Dispose(). PactBroker предоставляет перечень методов API для работы с ним, ознакомится с которыми можно в панели брокера. Для отправки пактов будем использовать метод pacts/provider/{provider}/consumer/{consumer}/version/{consumerVersion}.

Создадим класс для отправки пактов и назовем его PactBrokerPublisher. В рамках демо ограничимся простой логикой, суть класса сводится к вызову PUT метода по пути, представленному выше:

private readonly HttpClient _httpClient;      public PactBrokerPublisher(HttpClient httpClient) {_httpClient = httpClient;}      public async Task Publish(string consumer, string provider, string content, string consumerVersion) {     var response = await _httpClient     .PutAsync($"pacts/provider/{provider}/consumer/{consumer}/version/{consumerVersion}",     new StringContent(content)     {       Headers = { ContentType = new MediaTypeHeaderValue("application/json") }     });      if (response.IsSuccessStatusCode == false)         throw new ArgumentNullException($"Ошибка во время отправки пакта в PactBroker: {response.StatusCode}"); } 

Для отправки пактов в конце выполнения тестов всего класса создадим класс PactBrokerFixture и реализуем в нём интерфейс IDisposable. Цель класса заключается в отправке файла пактов PactBroker`у во время вызова метода Dispose().

private readonly Uri _pactBrokerUri = new ("http://localhost:9292"); private readonly string _pactUsername = "admin"; private readonly string _pactPassword = "pass"; private readonly PactBrokerPublisher _pactBrokerPublisher;  public string ConsumerVersion { get; set; }  public IPact? PactInfo { get; set; }      public PactBrokerFixture() {     var baseAuthenticationString = Convert.ToBase64String(System.Text.Encoding.ASCII.GetBytes($"{_pactUsername}:{_pactPassword}"));     _pactBrokerPublisher = new PactBrokerPublisher(new HttpClient     {         DefaultRequestHeaders =         {             Authorization = new AuthenticationHeaderValue("Basic", baseAuthenticationString)         },         BaseAddress = _pactBrokerUri     }); }      public void Dispose() {     Task.Run(async () =>     {         var versionSuffix = Guid.NewGuid().ToString().Substring(0, 5);         var pactJson = await File.ReadAllTextAsync($"{PactInfo.Config.PactDir}/{PactInfo.Consumer}-{PactInfo.Provider}.json");         await _pactBrokerPublisher.Publish(                 consumer: PactInfo.Consumer, provider: PactInfo.Provider, content: pactJson,                 $"{ConsumerVersion}-{versionSuffix}");     }); } 

Дело осталось за малым, выполним следующие шаги:

  1. Классы OrderCardTests и CardOrderSatisfiedEventTests реализуют интерфейс IClassFixture<PactBrokerFixture>, а также внедряют в конструктор зависимость PactBrokerFixture.

  2. Сборка Consumer.Domain имеет тег версии <Version>1.0.0</Version>.

  3. Конструкторы классов OrderCardTests и CardOrderSatisfiedEventTests записывают значения в свойства фикстуры: ConsumerVersion и PactInfo.

brokerFixture.PactInfo = pact; brokerFixture.ConsumerVersion = Assembly     .GetAssembly(typeof(CardOrderSatisfiedEvent))?     .GetCustomAttribute<AssemblyInformationalVersionAttribute>()?     .InformationalVersion;

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

Получение пактов из PactBroker

Для скачивания существующих пактов библиотека PactNet предоставляет метод WithPactBrokerSource(), вызов которого мы добавим в два наших теста на стороне поставщика.

_pactVerifier     ...     .WithPactBrokerSource(new Uri("http://localhost:9292"), options =>     {         options.BasicAuthentication("admin", "pass");         options.PublishResults(_providerVersion + $" {Guid.NewGuid().ToString().Substring(0, 5)}");     })     // .WithFileSource(new FileInfo(@"..\..\..\pacts\Demo.Consumer-Demo.Provider.json"))     ... 

Метод BasicAuthentication() отвечает за аутентификацию в PactBroker, учетные данные для которого были заданы в момент поднятия контейнеров. В свою очередь метод PublishResult() вызывать необязательно, поскольку он необходим лишь для отображения результатов верификации контракта поставщиком в панели PactBroker. Поле _providerVersion заполняется аналогично ConsumerVersion, который мы видели ранее, но тег версии уже принадлежит сборке Provider.Contracts.

Обзор панели PactBroker

Наконец оба проекта покрыты контрактными тестами, а сгенерированные пакты доставляются с помощью PactBroker. Последовательно запустим тесты в папке consumer и provider. Если все проверки прошли, то открыв в браузере страницу http://localhost:9292/, можно увидеть таблицу, отображающую имеющиеся у PactBroker пакты.

Домашнаяя страница панели брокера

Домашнаяя страница панели брокера

В таблице, согласно названиям столбцов, отображаются: название потребителя контракта, название поставщика контракта, дата последней публикации со стороны потребителя, наличие веб хука (не будет рассматриваться в данной статье) и дата последней проверки со стороны поставщика. Последний столбец, в зависимости от прохождения проверки на стороне поставщика, окрашивается в цвета: красный — проверка не пройдена, желтый — проверка еще не проводилась и зеленый — контракт успешно верифицирован на стороне поставщика.

Брокер хранит пакты в базе данных:

Хранилище пактов

Хранилище пактов

При нажатии на иконку документа открывается просмотр пактов. К сожалению, при использовании PactV4 файл пакта по каким-то причинам не преобразуется в удобный для чтения формат и отображается просто как JSON файл. В то же время предыдущие версии, вроде PactV3 успешно парсятся. Сравните:

Отображения пакта при использовании PactV4

Отображения пакта при использовании PactV4
Отображения пакта при использовании PactV3

Отображения пакта при использовании PactV3

Несмотря на то, что читать второй вариант удобнее, вариант с PactV4 все еще довольно информативен. Однако, если вам не приходится работать с кириллицей, можно использовать более стабильные версии библиотеки PactNet, в которых отображение пактов будет смотреться красивее.

Перейдем в матрицу контрактов между системами. В ней отображаются зависимости между системами, а также результаты верификаций. Как мы видим, контракт между Demo.Consumer версии 1.0.0-d1549 и Demo.Provider версии 1.0.0 соблюдается обеими сторонами. Но, если поставщик контракта вдруг внесет в контракт какое-то несогласованное изменение, то пакт между системами будет нарушен. Так, Demo.Provider версии 2.0.0 и Demo.Consumer версии 1.0.0-d1549 уже не смогут работать без ошибок.

Зависимости между системами

Зависимости между системами

При нажатии на значение в столбце Pact verified можно увидеть ошибку, которая также выводилась в консоль приложения:

Результат изменения названия поля на стороне поставщика

Результат изменения названия поля на стороне поставщика

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

Для демонстрации добавлено еще несколько провайдеров

Для демонстрации добавлено еще несколько провайдеров

Заключение

На этот момент это всё, чем хотелось бы поделится в отношении инструмента PactNet. На мой взгляд данная библиотека предоставляет достаточно мощный инструментарий для написания и поддержки действительно полезных тестовых сценариев. Несмотря на то, что объем материала за две статьи получился немалым, это далеко не все возможности Pact. В частности остались не рассмотренными такие возможности, как:

  • ProviderState — функционал для задания поставщику перед тестом некоторого состояния. К примеру, проверка статуса заказа подразумевает, что поставщик уже хранит сущность определенного заказа. Чтобы не поддерживать большой объем тестовых данных на стороне тестов поставщика, существует возможность непосредственно в тесте потребителя задать состояние второго участника. Пример реализации такого сценария можно найти в репозитории библиотеки PactNet;

  • Branches, tags — Pact имеет поддержку ветвления кода, лучшее применение которого раскрывается в совокупности с применением pact-cli;

  • pact-cli;

  • GraphQL API;

  • WebHooks;

  • Matchers — реализация под .NET все же несколько сырая по сравнению с PactJS;

  • Остальные методы PactBroker API, с помощью которых в теории можно сконструировать гибкое решение вообще без использования PactNet;

  • Много чего еще касательно конфигурирования, чтения пактов и т.д.

Контрактные тесты не являются чем-то обязательным, однако иногда, действительно помогают обнаруживать breaking changes на раннем этапе. Разумеется, как и любой инструмент, использовать такого рода тесты следует с умом.

Реализация библиотеки Pact для .NET предоставляет все основные возможности для написания контрактных тестов из коробки. К основным её минусам можно отнести отсутствие поддержки in-memory TestServer, отсутствие подробной документации (лишь готовые примеры реализации) и широкое использование типа dynamic, что в принципе обуславливается реализацией библиотеки. Несмотря на все вышеперечисленные минусы, достоинств у Pact все же больше, и надеюсь из двух статей стало понятно, в чём они заключаются.


ссылка на оригинал статьи https://habr.com/ru/articles/823082/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *