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

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

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

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

  • Имя очереди

  • Настройки, такие как ключ маршрутизации, темы, ответ и т. д.

Оборудование для демонстрации

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

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

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

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

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

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

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

Представим, что вслед за соглашением по договору записаны следующие соглашения:

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

  • Имя обменника SpecialExchangeName, введите Direct;

  • Для ключа маршрутизации установлено значение супер-ключа маршрутизации.

Чтобы реализовать этот сценарий, добавьте в сборки Consumer.Host и Provider.Host следующие зависимости:

 

Для простоты реализуем отправку сообщения непосредственно в контроллере сервиса Demo.Provider:

[HttpPost("order-satisfied/{userId}")]
public async Task 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(
          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();
      }).Advanced;
var exchange = advanced.ExchangeDeclare("SpecialExchangeName", "direct");
var queue = advanced.QueueDeclare("SpecialQueueName");
advanced.Bind(exchange, queue, routingKey: "super-routing-key");
advanced.Consume(queue, (message, _) =>
    Task.Factory.StartNew(() =>
    {
        var handler = app.Services.GetRequiredService();
        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

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

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

Давайте создадим его в папке Consumer.ContractTests/RabbitMq класс 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(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(msg =>
            {
                // Assert
                // место для вызова IConsumer.Handle и проверки логики работы обработчика
                //_consumerCardService.Verify(x => x.PushUser(msg), Times.Never);
            });
    }
}
  • Вместо IPactBuilderV4 использовал IMessagePactBuilderV4, который определяет пакты для систем, взаимодействующих с использованием брокеров сообщений. Объект создается путем вызова метода WithMessageInteractions(). Остальная часть кода конфигурации такая же, как и в HTTP-тестах;

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

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

ЧИТАТЬ   МТС расширила сеть Интернета вещей в агропромышленных зонах Воронежской области

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

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

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

Теперь создадим его в папке Поставщик.ContractTests/RabbitMq класс 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
        {
            {"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:порт/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
версия: «3.9» сервисы: postgres: изображение: postgres:15 имя_контейнера: порты pact-postgres: - «5432:5432» проверка работоспособности: тест: psql postgres -U postgres --command 'SELECT 1' среда: POSTGRES_USER: postgres POSTGRES_PASSWORD : postgres POSTGRES_DB: брокер postgres: изображение: pactfoundation/pact-broker:latest-multiContainer_name: pact-broker-1 depend_on: - порты postgres: - "9292:9292" перезапуск: всегда среда: PACT_BROKER_ALLOW_PUBLIC_READ: "false" PACT_BROKER_BASIC_AUTH_USERNAME: admin PACT_BROKER_BASIC_ AUTH_PASSWORD: пройти проверку работоспособности PACT_BROKER_DATABASE_URL: "postgres://postgres:postgres@postgres/postgres": test: ["CMD", "curl", "--silent", "--show-error", "--fail",
             "
      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 ("
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.

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

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

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

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

ЧИТАТЬ   Почему мужчинам важно идти к урологу, даже если их ничего не беспокоит - Лайфхакер

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

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

_pactVerifier
    ...
    .WithPactBrokerSource(new Uri(" 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 все же больше, и надеюсь из двух статей стало понятно, в чём они заключаются.

Source

От admin