Найдите утечки и затяните болты.

В моей последней статье я собрал конвейер, который построил и развернул три отдельных микросервиса. Но я обманул: все этапы тестирования были заполнителями. Добавим тесты.

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

Мое представление о тестах значительно изменилось с годами. Раньше я был большим специалистом по модульным тестам. При создании монолитов очень важно тестировать каждую часть по отдельности для раннего обнаружения ошибок, и единственный способ протестировать каждую часть по отдельности - это модульные тесты. Теперь в мире микросервисов каждый компонент может работать отдельно, и его можно тестировать отдельно и автоматически в (полу) реальной среде. Больше никаких макетов баз данных или HTTP-вызовов; просто разверните в среде и тестируйте автоматически каждый раз, когда вы отправляете код.

Я думаю, что модульные тесты по-прежнему важны, особенно для многоразовых библиотек. Никто не хочет использовать библиотеку кода, которая не была тщательно протестирована. Но с проектом MondoReacto, над которым я работал, подавляющая часть кода предоставляется Spring и другими сторонними библиотеками. Единственное, что мне осталось проверить, - это уникальную логику используемого микросервиса.

Я особо не говорил о проекте MondoReacto. Это был мой эксперимент по использованию реактивной Java, а именно projectreactor.io (отсюда и название MondoReacto). Реактивное программирование - это неблокирующий, управляемый событиями и свободный стиль программирования. Любой, кто знаком с библиотекой Java Stream, представленной в Java 8, должен чувствовать себя как дома с ее стилем. Вот фрагмент, который считывает вызов HTTP POST и записывает его в шину сообщений (в данном случае Kafka).

Так что здесь происходит? В реактивном программировании мы имеем дело с Fluxes и Monos, но они очень похожи на Streams. В приведенном выше фрагменте мы видим только Monos, что является частным случаем Fluxes, которые могут обрабатывать не более одного элемента.

Поскольку этот код принимает один элемент payload, преобразует его, отправляет в Kafka и возвращает код состояния Kafka, мы можем использовать Mono<Input> и Mono<String> в качестве входных и выходных данных. Библиотека WebFlux Spring имеет дело с Flux и Mono входами и выходами и отображает их на неблокирующий HTTP-сервер (в данном случае Netty).

Наша логика просто преобразует данные из входа в выход, поэтому это относительно просто. Мы преобразуем Input в String, String в ProducerRecord и, наконец, ProducerRecord в SenderRecord. Мы передаем это отправителю Kafka в качестве окончательного преобразования, Mono<SenderRecord>, и он возвращает Flux<SenderResult>. Поскольку мы знаем, что у нас есть только одна запись, мы можем преобразовать ее из Flux<SenderResult> в Mono<SenderResult> с помощью оператора publishNext и преобразовать SenderResult в String, подходящий для возврата из нашего сообщения.

Любой, кто знаком с библиотекой Java Stream, знает, что ничего не происходит, пока у вас нет оператора терминала. Создаваемые потоки просто берут функции (преобразования или фильтры), добавленные к потоку, и сохраняют их для последующей обработки. То же самое можно сказать и о библиотеке Reactor. Он сохраняет список преобразований или фильтров, а затем, когда Flux или Mono подписан, он передает блоки данных от одного к другому. В нашем случае подписка происходит в методе send получателя Kafka, который завершает первый поток и возвращает второй поток, на который подписана глубоко в недрах Netty, поэтому мы никогда не видим ни одну из подписок.

Ключ к тому, чтобы все это работало гладко, заключается в том, что ни один из методов сопоставления или фильтрации в конвейере не может блокироваться: нет вызовов базы данных или чтения файлов, которые могут блокироваться при вводе-выводе. Все вызовы должны быть асинхронными, и чтобы дождаться ответа, нам нужно создать Publisher, который реактивная система знает, как обрабатывать. К счастью, существует множество библиотек, которые работают с этой парадигмой, например, HTTP-контроллер WebFlux и интерфейс Kafka из приведенного выше фрагмента. В большинстве случаев все, что нам нужно сделать, это соединить части.

Давайте взглянем на тестовый фрагмент. Я использую Cucumber.io для платформы тестирования, которая использует язык Gherkin. Я собираюсь протестировать первый этап проекта MonodoReacto, который был описан выше, используя HTTP POST и отправляя данные в Kafka. Вот описание моего корнишона:

Feature: Incoming input is passed to message queue

  Scenario: Good input is passed to message queue
    Given a good input value
    When the incoming service post is called
    Then the return value should be 202
    And the input value should be found on the message queue

Каждый шаг должен иметь базовый код для его выполнения. Вот код шага для «Когда вызывается входящий сервисный пост»:

А вот код, который вызывается, когда выполняется шаг «И входное значение должно быть найдено в очереди сообщений»:

Опять же, это комбинация фильтров и карт. Я действительно хотел бы, чтобы не было комбинированного фильтра / карты, который я часто использую, чтобы избавиться от пустых Optionals. publishNext в конце означает, что мы ищем только следующий совпадающий элемент, а .filter(i -> i.equals(inputValue)) гарантирует, что проходят только совпадающие элементы. Остальные отбрасываются, и подписка прекращается после публикации первой.

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

Итак, у нас есть все это на месте. (У меня будет фактический код, ссылка на который приведена внизу этой статьи.) Теперь нам нужно запустить его в конвейере, созданном в предыдущей статье. Мы добавим сборку тестов в конвейер сборки Jenkins. Я собираюсь создать приложение и тест за один шаг, а затем запустить конвейер развертывания. Но сначала мне нужен способ запускать сборку, когда код изменяется в истинном стиле CI / CD.

Однако есть одна загвоздка. Jenkins обычно запускается при изменении репозитория, для которого он настроен. Наши конвейеры сборки настроены для репозитория сборки. Мы могли бы объединить наш репозиторий сборки и репозиторий кода, но мне нравится четкое разделение задач. Итак, я собираюсь создать новый конвейер, который смотрит на репозиторий кода, но ничего не делает, кроме запуска конвейера сборки. Я также собираюсь отправить себе уведомление Slack о том, что сборка запущена. Вот раздел SCM:

Вот раздел "Триггеры сборки":

Обратите внимание, я должен опросить SCM. Мой хост Jenkins находится за моим брандмауэром, поэтому я не могу заставить GitHub запускать его автоматически. Он устанавливается примерно каждые пять минут, пока я активно разрабатываю, но я установлю его или уберу, как только моя тяжелая разработка будет завершена. Я уверен, что если бы каждый пользователь GitHub в мире проводил опрос каждые пять минут, это было бы DDOS-атакой.

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

Затем я собираюсь изменить конвейер сборки, чтобы он собирал приложение и тестовые образы, а затем запускал конвейер развертывания. Вот первая часть:

Этапы Build… Image и Start… Service будут дублированы для каждого из других микросервисов: incoming-persist и incoming-read-service. Мне также придется скопировать desc/incoming-service-test.yaml в описание yamls для микросервисов incoming-persist-test и incoming-read-service-test. И, наконец, в конце Jenkinsfile я добавлю еще одно уведомление Slack.

post {
        always {
            slackSend (color: '#FFFF00', message: "Jenkins Build ${currentBuild.currentResult}: Job '${env.JOB_NAME} [${env.BUILD_NUMBER}]' (${env.BUILD_URL})")
        }
    }

Теперь я внесу изменения в проект MondoReacto и получу уведомления, когда сборка начинается и когда она останавливается:

Ой! (грустный звук тромбона) По крайней мере, у вас есть ссылка на сборку, которая не удалась. Оказывается, я случайно скопировал команду git на каждый этап, и она не удалась, потому что отказалась перезаписать старое репо. Я удалил все, кроме первой команды git, но это вызывает другую проблему. Эти рабочие узлы остаются на некоторое время и хранят репозиторий вместе с ними. Мы должны иметь возможность удалить репозиторий в конце конвейера, чтобы этого не произошло. Но пока я просто удалю лишние команды git и удалю рабочий модуль maven перед перезапуском конвейера. Запускаем еще раз, и все работает!

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

Работали только входящие сервисные тесты. Как я сказал в своей прошлой статье, MondoReacto - это WIP. Я исправлю проблемы в других тестах и ​​не буду утомлять вас подробностями. Проект MondoReacto был для меня учебным проектом, чтобы разобраться в реактивном программировании, и, вероятно, в тестах есть какое-то наивное использование.

Еще впереди: я хочу пометить репозиторий и изображение, когда оно перемещается по конвейеру, чтобы я знал, какая версия последней прошла через разные этапы. Я также хочу больше тестов по мере прохождения между этапами, возможно, некоторые тесты производительности. И хотелось бы лучшего логирования с красивым интерфейсом вроде Kibana.

Но на данный момент у меня есть конвейер для создания, тестирования и развертывания трех отдельных микросервисов в трех разных средах, и все они запускаются путем отправки на GitHub. Скрипты конвейера для этой статьи можно найти здесь: https://github.com/rkamradt/MondoReactoBuild/tree/v1.2, а код можно найти здесь: https://github.com/rkamradt/MondoReacto /tree/1.1