Русский
Русский
English
Статистика
Реклама

Junit

Выбор библиотеки ассертов для проекта на Kotlin

09.07.2020 10:15:09 | Автор: admin

В одном из старых проектов в кучу были навалены ассерты из JUnit, kotlin.test и AssertJ. Это было не единственной его проблемой: его вообще писали как письмо Дяди Федора, а времени остановиться и привести к единому виду не было. И вот это время пришло.


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


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



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


Немного бэкграунда


Долгое время моим основным ЯП была Scala, а фреймворком для тестов ScalaTest. Не то чтобы это был лучший фреймворк, но я как-то к нему привык, поэтому мое мнение может быть искажено из-за этого.


Когда на старой работе начали писать на Kotlin, через какое-то время даже делали свою библиотечку, имитирующую поведение скаловских матчеров, но сейчас она имеет мало смысла после появления Kotest (хотя в ней лучше было реализовано сравнение сложных структур, с рекурсивным выводом более подробного сообщения).


Требования


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


  1. Бесшовная интеграция с Kotlin и IntelliJ Idea. Scala-библиотеки по этому принципу отпадают извращаться с настройкой двух рантаймов нет желания. Несмотря на это, ScalaTest будет присутствовать в сравнении как отправная точка, просто потому что с ним много работал. Под интеграцией с IntelliJ я подразумеваю возможность клика на <Click to see difference>, чтобы увидеть сравнение реального значения и ожидаемого. Эта фича, вообще говоря, работает в кишках IntelliJ Idea но ведь разработчики Kotlin-библиотек наверно про нее все-таки слышали и могут решить эту проблему, да?
  2. Возможность быстро понять проблему. Чтобы было не 1 != 2 и стектрейс, а нормальное сообщение, содержащее в идеале название переменной и разделение на "expected" и "actual". Чтобы для коллекций было сообщение не просто "два множества на 100 элементов не равны, вот тебе оба в строковом представлении, ищи разницу сам", а подробности, например " эти элементы должны быть, а их нет, а вот эти не должны, но они есть". Можно конечно везде описания самому писать, но зачем тогда мне библиотека? Как выяснилось чуть позже, название переменной это была влажная мечта, и при недолгих раздумьях будет очевидно, что это не так-то просто сделать.
  3. Адекватность записи. assertEquals(expected, actual) Йоды стиль читать сложно мне, вкусовщина однако это большая. Кроме того, я не хочу задумываться о тонких нюансах библиотеки в идеале должен быть ограниченный набор ключевых слов/конструкций, и чтобы не надо было вспоминать особенности из серии "это массив, а не коллекция, поэтому для него нужен особый метод" или помнить, что строка не contains, а includes. Другими словами это одновременно читаемость и очевидность как при чтении, так и при написании тестов.
  4. Наличие проверки содержания подстроки. Что-то вроде assertThat("Friendship").contains("end").
  5. Проверка исключений. В качестве контр-примера приведу JUnit4, в котором исключение ловится либо в аннотацию, либо в переменную типа ExpectedException с аннотацией @Rule.
  6. Сравнение коллекций и содержание элемента(ов) в них.
  7. Поддержка отрицаний для всего вышеперечисленного.
  8. Проверка типов. Если ошибка будет выявлена компилятором то это гораздо круче, чем если она будет выявлена при запуске теста. Как минимум, типизация не должна мешать: если мы знаем тип ожидаемого значения, то тип реального значения, возвращенного generic-функцией, должен быть выведен. Контр-пример: assertThat(generic<Boolean>(input)).isEqualTo(true). <Boolean> тут лишний. Третий вариант заключается в игнорировании типов при вызове ассерта.
  9. Сравнение сложных структур, например двух словарей с вложенными контейнерами. И даже если в них вложен массив примитивов. Все же знают про неконсистентность их сравнения? Так вот ассерты это последнее место, где я хочу об этом задумываться, даже если это отличается от поведения в рантайме. Для сложных структур по-хорошему должен быть рекурсивный обход дерева объектов с полезной информацией, а не тупо вызов equals. Кстати в той недо-библиотеке на старой работе так и было сделано.

Не будем рассматривать сложные поведенческие паттерны или очень сложные проверки, где надо у объекта человек прям сразу проверить, что он и швец, и жнец, и на дуде играет, и вообще хороший парень в нужном месте и время. Повторюсь, что цель подобрать ассерты, а не фреймворк для тестов. И ориентир на разработчика, который пишет юнит-тесты, а не на полноценного QA-инженера.


Конкурсанты


Перед написанием этой статьи я думал, что достаточно будет сравнить штук 5 библиотек, но оказалось, что этих библиотек пруд пруди.


Я сравнивал следующие библиотеки:


  1. ScalaTest как опорная точка для меня.
  2. JUnit 5 как опорная точка для сферического Java-разработчика.
  3. kotlin.test для любителей multiplatform и официальных решений. Для наших целей это обертка над JUnit, но есть нюансы.
  4. AssertJ довольно популярная библиотека с богатым набором ассертов. Отпочковалась от FestAssert, на сайте которого по-японски торгуют сезамином по всем старым ссылкам на документацию.
  5. Kotest он же KotlinTest, не путать с kotlin.test. Разработчики пишут, что их вдохновлял ScalaTest. В кишках есть даже сгенерированные функции и классы для 1-22 аргументов в лучших традициях scala.
  6. Truth библиотека от Гугла. По словам самих создателей, очень напоминает AssertJ.
  7. Hamсrest фаворит многих автотестировщиков по мнению Яндекса. Поверх нее еще работает valid4j.
  8. Strikt многим обязан AssertJ и по стилю тоже его напоминает.
  9. Kluent автор пишет, что это обертка над JUnit (хотя на самом деле над kotlin.test), по стилю похож на Kotest. Мне понравилась документация куча примеров по категориям, никаких стен текста.
  10. Atrium по словам создателей, черпали вдохновение из AssertJ, но потом встали на свой путь. Оригинальная особенность локализация сообщений ассертов (на уровне импорта в maven/gradle).
  11. Expekt черпали вдохновение из Chai.js. Проект заброшен: последний коммит 4 года назад.
  12. AssertK как AssertJ, только AssertK (но есть нюансы).
  13. HamKrest как Hamсrest, только HamKrest (на самом деле от Hamcrest только название и стиль).

Если вы хотите нарушить прекрасное число этих конкурсантов пишите в комментах, какую достойную сравнения библиотеку я упустил или делайте пулл-реквест.


Эволюция методики оценки


Когда уже написал процентов 80 статьи и добавлял все менее известные библиотеки, наткнулся на репозиторий, где есть сравнение примерно в том виде, что мне думалось изначально. Возможно кому-то там проще будет читать, но там меньше конкурсантов.


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


Потом решил, что все-таки надо защититься от банальных ошибок, и средствами JUnit сделал валидатор, который проверяет, что все тесты, которые должны были завалиться, завалились, и что нет неизвестных тестов. Когда наткнулся на баг в ScalaTest, решил сделать две вариации: одна, где все тесты проходят, вторая где ничего не проходит и дополнил валидатор. Внимательный читатель может спросить: а кто стрижет брадобрея и какие ассерты использованы там? Отчасти для объективности, отчасти для переносимости ассертов там вообще нет:). Заодно будет демо/аргумент для тех, кто считает, что ассерты не нужны вообще.


Затем я оказался на распутье: выносить ли или нет константы типа listOf(1,2,3)? Если да то это упоротость какая-то, если нет то при переписывании теста на другие ассерты обязательно ошибусь. Составив список библиотек, которые стоит проверить для претензии на полноту исследования, я плюнул и решил решить эту проблему наследованием: написал общий скелет для всех тестов и сделал интерфейс для ассертов, который нужно переопределить для каждой библиотеки. Выглядит немного страшновато, зато можно использовать как розеттский камень.


Однако есть проблема с параметризованными проверками и type erasure. Reified параметры могут быть только в inline-функциях, а их переопределять нельзя. Поэтому хорошо заиспользовать конструкции типа


assertThrows<T>{...}

в коде не получится, пришлось использовать их дополнения без reified параметра:


assertThrows(expectedClass){...}

Я честно немного поковырялся в этой проблеме и решил на нее забить. В конце концов, в kotlin.test есть похожая проблема с интерфейсом Asserter: ассерт на проверку исключения в него не включен, и является внешней функцией. Чего мне тогда выпендриваться, если у создателей языка та же проблема?:)


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


Результаты


Дальше тупо суммируем баллы по требованиям: 0 если требование не выполнено, 0.5 если выполнено частично, 1 если все в целом ок. Максимум 9 баллов.


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


Библиотека Интеграция Описание ошибки Читабельность Подстрока Исключения Коллекции Отрицания Вывод типов Сложные структуры Итого
Kotest + + + + + нет - 6.0
Kluent + + + + + нет - 6.0
AssertJ + + + + нет 6.0
Truth + + + - + + нет - 5.5
Strikt + + + + нет - 5.5
ScalaTest + + + + нет - 5.5
HamKrest - + + + да - 5.5
AssertK + + + нет - 5.0
Atrium + + + нет - 5.0
Hamсrest + - + да - 5.0
JUnit + + - + - игнор - 4.5
kotlin.test + - - + - - да - 3.5
Expekt - + - + нет - 3.5

Примечания по каждой библиотеке:


Kotest
  • Чтобы подключить только ассерты, надо немного поковыряться. На мой взгляд сходу это понять тяжело.
  • Не имеет варианта ловли исключения с явным параметром, а не reified, но это на самом деле не особо и нужно: мало кто будет заниматься такими извращениями.
  • Сложные структуры: тест с вложенными массивами не прошел. Я завел на это тикет.
  • Интеграция: <Click to see difference> есть только для простых ассертов.
  • Типизация: иногда при использовании дженериков надо писать им явный тип.
  • Описание ошибок: почти идеальное, не хватило только подробностей отличия двух множеств.

Kluent
  • Можно писать как "hello".shouldBeEqualTo("hello"), так и "hello" `should be equal to` "hello". Любителям DSL понравится.
  • Интересная запись для ловли исключения:
    invoking { block() } shouldThrow expectedClass.kotlin withMessage expectedMessage
    
  • Описания ошибок в целом отличные, не нет подробностей отличия двух коллекций, что не так. Еще расстроила ошибка в формате Expected Iterable to contain none of "[1, 3]" непонятно, что на самом деле проверяемый Iterable содержит.
  • Интеграция: <Click to see difference> есть только для простых ассертов.
  • Сложные структуры: тест с вложенными массивами не прошел.

AssertJ
  • Впечатляющее количество методов для сравнения еще бы запомнить их Надо знать, что списки надо сравнивать через containsExactly, множества через hasSameElementsAs, а словари через .usingRecursiveComparison().isEqualTo.


  • Интеграция: нет <Click to see difference>.


  • Исключения: ловится просто какое-то исключение, а не конкретное. Сообщение об ошибке, соответственно, не содержит название класса.


  • Сложные структуры: есть .usingRecursiveComparison(), который почти хорошо сравнивает. Однако ошибку хотелось бы иметь поподробнее: ассерт определяет, что значения по одному ключу не равны, но не говорит по какому. И несмотря на то, что он корректно определил, что два словаря с массивами равны, отрицание этого ассерта


    assertThat(actual)    .usingRecursiveComparison()    .isNotEqualTo(unexpected)
    

    сработало некорректно: для одинаковых структур не завалился тест на их неравенство.


  • Типизация: иногда при использовании дженериков надо писать им явный тип. Видимо, это ограничение DSL.



Truth
  • Подробные сообщения об ошибках, иногда даже многословные.
  • Исключения: не поддерживаются, пишут, что надо использовать assertThrows из JUnit5. Интересно, а если ассерты не через JUnit запускают, то что?
  • Читаемость: кроме прикола с исключением, странное название для метода, проверяющего наличие всех элементов в коллекции: containsAtLeastElementsIn. Но я думаю на общем фоне это незначительно, благо тут можно для сравнения коллекций не задумываясь писать assertThat(actual).isEqualTo(expected).
  • Интеграция: <Click to see difference> только для примитивного ассерта.
  • Сложные структуры: тест с вложенными массивами не прошел.
  • Типизация: тип не выводится из ожидаемого значения, пришлось писать явно.
  • Веселый стектрейс с сокращалками ссылок "для повышения читаемости":
    expected: 1but was : 2at asserts.truth.TruthAsserts.simpleAssert(TruthAsserts.kt:10)at common.FailedAssertsTestBase.simple assert should have descriptive message(FailedAssertsTestBase.kt:20)at [[Reflective call: 4 frames collapsed (http://personeltest.ru/aways/goo.gl/aH3UyP)]].(:0)at [[Testing framework: 27 frames collapsed (http://personeltest.ru/aways/goo.gl/aH3UyP)]].(:0)at java.base/java.util.ArrayList.forEach(ArrayList.java:1540)at [[Testing framework: 9 frames collapsed (http://personeltest.ru/aways/goo.gl/aH3UyP)]].(:0)at java.base/java.util.ArrayList.forEach(ArrayList.java:1540)at [[Testing framework: 9 frames collapsed (http://personeltest.ru/aways/goo.gl/aH3UyP)]].(:0)at java.base/java.util.ArrayList.forEach(ArrayList.java:1540)at [[Testing framework: 17 frames collapsed (http://personeltest.ru/aways/goo.gl/aH3UyP)]].(:0)at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor$CollectAllTestClassesExecutor.processAllTestClasses(JUnitPlatformTestClassProcessor.java:99)...
    

Strikt
  • Не имеет варианта ловли исключения с явным параметром, а не reified, но это на самом деле не особо и нужно: мало кто будет заниматься такими извращениями.


  • Отрицание для содержания подстроки в строке выглядит неконсистентно: expectThat(haystack).not().contains(needle), хотя для коллекций есть нормальный expectThat(collection).doesNotContain(items).


  • Читаемость: для массивов надо использовать contentEquals. Та же проблема с отрицанием: expectThat(actual).not().contentEquals(unexpected). Более того, надо еще думать о типе, потому что для Array<T> Strikt почему-то не смог определить нужный ассерт сам. Для списков containsExactly, для множеств containsExactlyInAnyOrder.


  • Типизация: иногда при использовании дженериков надо писать им явный тип. Более того, для массивов нужно еще с вариацией правильно тип подобрать. Посмотрите на этот кусочек кода:


    val actual: Array<String> = arrayOf("1")val expected: Array<String> = arrayOf("2")expectThat(actual).contentEquals(expected)
    

    Он не скомпилируется, потому что компилятор не сможет определить перегрузку для contentEquals. Это происходит потому, что нужный contentEquals определен с ковариантным типом:


    infix fun <T> Assertion.Builder<Array<out T>>.contentEquals(other: Array<out T>)
    

    Из-за этого надо писать


    val actual: Array<out String> = arrayOf("1")val expected: Array<String> = arrayOf("2")expectThat(actual).contentEquals(expected)
    

  • Интеграция: нет <Click to see difference>.


  • Описание ошибки: нет подробностей для словаря и массивов, а целом довольно подробно.


  • Сложные структуры: тест с вложенными массивами не прошел.



ScalaTest
  • Интеграция: при сравнении коллекций нельзя открыть сравнение.
  • Описание ошибки: в коллекциях написано, что просто не равны. Для словаря тоже подробностей нет.
  • Читабельность: надо помнить об особенностях DSL при отрицании и contains, отличии contains и include, а также необходимости theSameElementsAs.
  • Сложные структуры: тест с вложенными массивами не прошел, на это есть тикет.
  • Типизация: тип не выводится из ожидаемого значения, пришлось писать явно.

HamKrest
  • Проект, судя по тикетам, в полузаброшенном состоянии. Вдобавок документация весьма жиденькая пришлось читать исходный код библиотеки, чтобы угадать название нужного матчера.
  • Ожидал, что достаточно сменить импорты Hamcrest, но не тут-то было: довольно многое тут по-другому.
  • Запись ловли исключений зубодробительная:
    assertThat( {    block()}, throws(has(RuntimeException::message, equalTo(expectedMessage))))
    
  • Коллекции: нет проверки наличия нескольких элементов. Пулл-реквест висит 3,5 года. Написал так: assertThat(collection, allOf(items.map { hasElement(it) })).
  • Поддержки массивов нет.
  • Сложные структуры: тест с вложенными массивами не прошел.
  • Интеграция: нет <Click to see difference>.
  • Описание ошибки как-то ни о чем:
    expected: a value that not contains 1 or contains 3but contains 1 or contains 3
    

AssertK
  • Как можно догадаться из названия почти все совпадает с AssertJ. Однако синтаксис иногда немного отличается (нет некоторых методов, некоторые методы называются по-другому).


  • Читаемость: Если в AssertJ написано assertThat(collection).containsAll(items), то в AssertK та же конструкция сработает неправильно, потому что в нем containsAll принимает vararg. Понятно, что цель на containsAll(1,2,3), но продумать альтернативный вариант стоило бы. В некоторых других библиотеках есть похожая проблема, но в них она вызывает ошибку компиляции, а тут нет. Причем разработчикам проблема известна это один из первых тикетов. Вдобавок, нужно отличать containsOnly и containsExactly.


  • Интеграция: нет <Click to see difference>.


  • Исключения: ловится просто какое-то исключение, а не конкретное, потом его тип надо отдельно проверять.


  • Сложные структуры: аналога .usingRecursiveComparison() нет.


  • Описания ошибок подробности есть (хоть и не везде), но местами странные:


    expected to contain exactly:<[3, 4, 5]> but was:<[1, 2, 3]>at index:0 unexpected:<1>at index:1 unexpected:<2>at index:1 expected:<4>at index:2 expected:<5>
    

    Вот почему тут на первый индекс два сообщения?


  • Типизация: иногда при использовании дженериков надо писать им явный тип.



Atrium
  • Поставляется в двух вариантах стиля: fluent и infix. Я ожидал отличий вида assertThat(x).isEqualTo(y) против x shouldBe y, но нет, это expect(x).toBe(y) против expect(x) toBe y. На мой взгляд весьма сомнительная разница, с учетом того, что инфиксный метод можно вызвать без "инфиксности". Однако для инфиксной записи иногда нужно использовать объект-заполнитель o: expect(x) contains o atLeast 1 butAtMost 2 value "hello". Вроде объяснено, зачем так, но выглядит странно. Хотя в среднем по больнице мне нравится infix-ассерты (вертолеты из-за скаловского прошлого), для Atrium я писал во fluent-стиле.
  • Читабельность: странные отрицания: notToBe, но containsNot. Но это не критично. Пришлось думать, как сделать проверку наличия нескольких элементов в коллекции: contains принимает vararg, а containsElementsOf не может вывести тип, сделал тупой каст. Понятно, что цель на contains(1,2,3), но продумать альтернативный вариант стоило бы. Отрицание наличия нескольких элементов записывается как expect(collection).containsNot.elementsOf(items).
  • Поддержки работы с массивами нет, рекомендуют преобразовывать через toList.
  • Не имеет варианта ловли исключения с явным параметром, а не reified, но это на самом деле не особо и нужно: мало кто будет заниматься такими извращениями.
  • Сложные структуры: тест с вложенными массивами не прошел.
  • Интеграция: нет <Click to see difference>.
  • Описание ошибки: местами нет подробностей (при сравнении словарей, например), местами описание довольно запутанное:
    expected that subject: [4, 2, 1]        (java.util.Arrays.ArrayList <938196491>)does not contain: an entry which is: 1        (kotlin.Int <211381230>)  number of such entries: 1      is: 0        (kotlin.Int <1934798916>)  has at least one element: true      is: true
    
  • Типизация: иногда при использовании дженериков надо писать им явный тип.

Hamcrest
  • Читабельность: странный синтаксис для отрицаний (либо


    assertThat(actual, `is`(not(unexpected)))
    

    либо


    assertThat(actual, not(unexpected))
    

    Надо знать нюанс containsString vs contains vs hasItem vs hasItems. Пришлось думать, как сделать проверку наличия нескольких элементов в коллекции: hasItems принимает vararg, а Set<T> без знания T просто так не преобразуешь в массив. Понятно, что цель на hasItems(1,2,3), но продумать альтернативный вариант стоило бы. Получилось в итоге


    assertThat(collection, allOf(items.map { hasItem(it) }))
    

    С отрицанием еще веселее:


    assertThat(collection, not(anyOf(items.map { hasItem(it) })))
    

  • В продолжение этой вакханалии с hasItems, я поставил в графу "коллекции", потому что лучше б не было ассертов, чем такие.


  • Исключения: отдельной проверки нет.


  • Интеграция: нет <Click to see difference>.


  • Описание ошибки: для коллекций нет подробностей.


  • Сложные структуры: тест с вложенными массивами не прошел.



JUnit
  • Читабельность: Йода-стиль assertEquals(expected, actual), надо помнить нюансы и отличия методов: что массивы надо сравнивать через assertArrayEquals, коллекции через assertIterableEquals и т.п.
  • Описание ошибок: для тех случаев, когда у JUnit все-таки были методы, оно было вполне нормальным.
  • Подстрока: через assertLinesMatch(listOf(".*$needle.*"), listOf(haystack)) конечно можно, но выглядит это не очень.
  • Отрицания: нет отрицания для assertLinesMatch, что логично, нет отрицания для assertIterableEquals.
  • Коллекции: нет проверки содержания элемента, assertIterableEquals для Map и Set не подходит совсем, потому что ему важен порядок.
  • Сложные структуры: тупо нет.

kotlin.test
  • Очень бедно. Вроде как это должна быть обертка над JUnit, но методов там еще меньше. Очевидно, что это расплата за кроссплатформенность.
  • Проблемы те же, что и у JUnit, и плюс к этому:
  • Нет проверки подстроки.
  • Нет даже намека на сравнения коллекций в лице assertIterableEquals, нет сравнения массивов.
  • Типизация: JUnit'у пофиг на типы в assertEquals, а kotlin.test ругнулся, что не может вывести тип.
  • Описание ошибок: не по чему оценивать.

Expekt
  • Можно писать в двух стилях expect(x).equal(y) и x.should.equal(y), причем второй вариант не инфиксный. Разница тут ничтожна, выбрал второй.
  • Читабельность: contains(item) против should.have.elements(items) и should.contain.elements(items). Причем есть приватный метод containsAll. Пришлось думать, как сделать проверку наличия нескольких элементов в коллекции: should.have.elements принимает vararg. Понятно, что цель на should.have.elements(1,2,3), но продумать альтернативный вариант стоило бы. Для отрицания нужно еще вспомнить про any: .should.not.contain.any.elements.
  • Типизация: тип не выводится из ожидаемого значения, пришлось писать явно.
  • Поддержки исключений нет.
  • Поддержки массивов нет.
  • Сложные структуры: тест с вложенными массивами не прошел.
  • Описание ошибки: просто разный текст для разных ассертов без подробностей.
  • Интеграция: нет <Click to see difference>.

Заключение


Лично мне из всего этого разнообразия понравились Kotest, Kluent и AssertJ. В целом я в очередной раз опечалился тому, как фигово работать с массивами в Kotlin и весьма удивился, что нигде кроме AssertJ нет нормального рекурсивного сравнения словарей и коллекций (да и там отрицание этого не работает). До написания статьи я думал, что в библиотеках ассертов эти моменты должны быть продуманы.


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

Подробнее..

Как законтрибьютить в опенсорс, чтобы не сгореть со стыда

19.01.2021 12:23:47 | Автор: admin

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


На осеннем TechTrain Андрей Солнцев (asolntsev) и Артем Ерошенко (eroshenkoam) показали на примере Allure и Selenide, как справиться с техническими и психологическими трудностями. Прямо во время доклада они сделали изменения в опенсорсных проектах.



Под катом расшифровка их доклада и видео с фестиваля. Далее повествование будет от лица спикеров.



Зачем


Андрей: Начнем с простых вопросов, которые вам интересны. Зачем вам это нужно? Зачем люди занимаются опенсорсом?


Лично я занимаюсь опенсорсом, потому что мне это интересно. Все остальные ответы меркнут и не важны. Но можно найти другие ответы, если вам этого мало.


Занятие опенсорсными проектами хорошая возможность прокачать скиллы.


Артём: Например, я очень много читаю код опенсорсных проектов вместо того, чтобы читать статьи. Я в основном делаю поиск по GitHub. Раньше я брал код из StackOverflow, вставлял его в свой код, и все работало. Сейчас я использую GitHub как источник примеров.


Например, недавно я искал, как настроить пайплайны в YAML Bamboo. Мне нужен был определенный пример конфигурации. Я нашел пример в документации вам нужен такой-то класс. А примера, как его использовать, не было. Вставляете класс в GitHub, смотрите примеры, и у вас получаются хорошие ответы, которые можно сравнить и выбрать лучший.


Андрей: Я научился многим вещам благодаря тому, что занимаюсь опенсорсом: владению гитом, все эти ветки, мерджи, ребейзы.


Еще опенсорс это отличная возможность прокачать свой профиль на GitHub. С тех пор, как я начал активно заниматься опенсорсом, меня заваливают вот такими письмами: Привет, нам понравился твой профиль на GitHub, давай к нам.


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



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


Артём: Скорее всего, у него есть основное место работы, он не заинтересован в большом количестве спонсоров. Но я знаю проекты, которые разрабатываются полностью в опенсорсе. Там люди не работают в других местах, и им нужно получать спонсорскую поддержку.


Андрей: Самый важный пункт для меня самовыражение. Есть пирамида потребностей айтишника.



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


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


Как поддержать опенсорс?


Лайки или деньги


Самый простой способ. Чтобы поддержать лайками, достаточно зайти на страничку любого опенсорс-проекта.


Любой проект с кнопкой Sponsor можно поддержать деньгами. Посмотрите, кто эти люди, чем занимаются, зачем им нужны деньги. Есть разные варианты:


  • заплатить самому;
  • уговорить свою компанию;
  • подписаться на платный саппорт.

Не знаю, как вы, а я встречал много проектов, которые мы все используем, и вдруг они умерли, потому что их больше никто не поддерживает. Таким образом их можно спасти.


Ответы на вопросы


Помочь можно не только деньгами или кодом. Все обычно думают, что нужно коммитить, уметь программировать. Так вот, мейнтейнерам нужен не только код и даже не столько он. Вы можете попробовать отвечать на вопросы других людей в форумах, в чатиках, на StackOverflow и еще на куче подобных сайтов. Это уже большая помощь.


Когда я запустил Selenide, то первые два года сам регистрировался во всех чатиках и отвечал на вопросы людей. Впервые я понял: О, началось, когда увидел, что на вопрос чувака о Selenide ответил другой чувак. И теперь на некоторые вопросы я не успеваю ответить, потому что прилетает ответ от другого. Для меня это огромная польза.


Артём: Это снимает с разработчика очень много нагрузки, потому что коммьюнити большое и ты физически не сможешь ответить на все вопросы. Без коммьюнити невозможно что-то нормально развернуть.


Пост в блоге


Андрей: Это может показаться мелочью: зачем писать пост, если о Selenide и так много написано?


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


Выступление на митапе


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


Помощь с кодом


Пойдем ближе к коду: можно оценить feature request. У меня, например, для Selenide поступает много feature requests. Я зачастую даже не умею оценить идею. Огромную ценность для меня представляют комментарии типа: Ой, не надо этого делать, такое уже есть.


Также вы можете повторить issue, который сложно повторяется. Еще вы можете проревьюить pull request или дополнить документацию.


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


Примеры


Сейчас посмотрим, как все эти пункты выглядят на практике.


Как заценить feature request?


  1. Идите в свой любимый проект на GitHub, откройте папку issues: там есть не только баги, но и вопросы, пожелания и pull requests.
  2. Ищите, где нет ясности.


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


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


Пример плохого комментария, где никто не поймет, почему идея дурацкая и кто ты вообще такой:



Вот пример хорошего комментария:



Дайте как можно больше контекста: у нас тоже есть такая проблема, мы научились обходить ее вот так, но этот способ ненадежный. Если бы это было в Selenide, то было бы круто. Мы сразу вникаем в контекст и понимаем, нужна фича или нет.


Мой любимый комментарий обычно выглядит так:



Это мой любимый комментарий, потому что этот issue можно сразу закрывать и ничего не делать.


Вы можете повторить issue. Очень часто бывает так, что в опенсорс-проекте висит куча каких-то багов, которые сложно или невозможно повторить. И мейнтейнеры рады бы их исправить, но не знают, как.


Вот пример из известного опенсорс HtmlUnit:



У HtmlUnit долго висел такой баг, но никто не знал, как его повторить, и иногда люди писали, что у них такая же проблема. И у меня однажды случилось так, что когда я гонял тесты Selenide, то случайно нащупал, как она у меня стабильно повторяется. Я смог написать такой вариант Selenide-тестов, где этот баг повторялся довольно стабильно. Я сделал отдельную ветку, в ней сделал этот вариант с повторением и заслал чувакам. И буквально через день чувак написал ответ:



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


Артём: Я занимаюсь разработкой большого количества инструментов, и зачастую для меня исправление проблемы стоит одну единицу времени, а ее воспроизведение стоит 7 единиц. Между нашим кодом и пользователем Allure находится много слоев, и мне приходится тратить время на воспроизведение проблемы.


Например, мне пишет человек, что у него не работает интеграция с Jira. Я поднимаю стенд прямо в облаке и говорю: Вот рабочий пример. А у него все равно не работает. Поэтому если бы мне прислали вот такой пример, я был бы счастлив. Это действительно хороший способ помочь авторам опенсорс-фреймворков.


Андрей: Вы можете проревьюить pull request. Зайдем в Files changed. Здесь вы можете создать свой code review на эти изменения. Написать, что именно плохо, чем плох стиль или посоветовать, как переименовать класс.



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


Нажимаем Review changes и оставляем какой-нибудь ревью.



Это плохой пример. Лучше конструктивно говорить, почему плохо, как лучше исправить. Но можно и так оставить.


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


Как поменять код


Есть два способа: легкий и продвинутый.


Легкий способ хорош тем, что там не надо вообще уметь пользоваться гитом, что-то компилировать. Многие о нем не знают, но на самом деле, это крутейший способ.



Будучи совершенно посторонним человеком с GitHub-аккаунтом, вы можете нажать Edit. Вам выдается предупреждение.



Исправляем, например, грамматические ошибки. Можно написать дополнительные комментарии о том, что ты сделал: fix typos. Обычно typos знакомо всем мейнтейнерам. Это кодовое слово, с которым, скорее всего, вас быстро смёрджат, потому что это очень простое изменение.



Все эти вещи подходят по умолчанию, GitHub за вас их заполняет правильно.



Далее мы выбираем Bug-fix, потому что это не новая фича и не breaking change. Больше ничего особенно и не надо.



И нажимаем волшебную кнопку Create pull request. Находясь в этом pull request, на всякий случай можем проверить, что все изменения есть, все строчки изменены. Еще важно проверять, что билд зеленый, потом еще вернуться и посмотреть, смёрджил он его или нет.


Отслеживать статус очень важный пункт, потому что я очень часто вижу, что в Selenide люди оставляют какой-то issue и уходят. А я, допустим, захожу, пишу свой комментарий или дополнительный вопрос, а они не отвечают.


Еще один лайфхак: если у вас есть проект, за которым вы хотите следить, знать о новых фичах и мёрджах, можно нажать на любом проекте кнопку Watch. Это позволяет получать уведомления о важных событиях.



Это пример, где в результате pull request полетел еще автоматический билд. Здесь можно отслеживать, какой у вас статус. Вы могли сломать какие-то тесты своим изменением. И самое важное: не бросайте! Вернитесь потом и посмотрите, что ваши изменения смёрджили или задали дополнительный вопрос.


Лёгкий способ хорош для:


  • документации;
  • сайта;
  • форматирования;
  • Javadoc;
  • исправления опечаток;
  • обновления зависимостей.

Артём: GitHub скоро выкатывает IDE, которая будет прямо в GitHub. И все эти штуки на GitHub будет легче делать. Сейчас у них уже выкатилась подсветка, когда ты можешь кликнуть переменную и посмотреть, где она используется. Естественно, большой рефакторинг так не сделать, но для небольшого pull request достаточно будет IDE, встроенной в GitHub.


Андрей: Продвинутый способ это когда вам нужно уже зачекаутить проект и менять его на своем компе: компилировать, запускать тесты.


Сначала вы должны сделать fork (клон) проекта в вашем GitHub-аккаунте, зачекаутить его на свою машину и реально поменять код, тест и все, что требует изменений.


Потом запустить локально какой-то минимальный набор тестов, проверить, что там ничего не сломано. И тогда уже коммитить и пушить. Когда вы запушите его, в GitHub появится pull request, и вам остается только следить за его статусом: что его наконец-то уже смёрджили или попросили что-то исправить. Здесь важно не просто его оставить, а довести дело до конца. Иногда приходится заходить и напоминать.


Практическая часть


Пример 1: Андрей -> Allure


Открываем проект Allure и находим issue, который можно исправить. Нашел три штуки, которые как-то связаны с Selenide. Похоже на то, что это не баги.



Этот пример связан не с Selenide, но это баг, который не похож на баг. Или, по крайней мере, баг не в Allure. Я залез в код Allure, проверил стектрейсы и пришел к выводу, что в Allure исправлять нечего.


Похоже, это баг не в Allure, а где-то в другом месте. И он повторяется в какой-то старой версии Java, на новой версии он никак не повторился. Поэтому я ему написал: Чувак, ты точно уверен? Ты пробовал повторить на более новой версии? Возможно, там уже это исправлено.


Обычно я вешаю лейбл на issue, где автор не отвечает, чтобы потом можно было понять, что тут мы ждем ответ. Если он долго не отвечает, мы просто закрываем issue. На самом деле закрыть issue не страшно, потому что когда он очнется, то оставить комментарий можно даже в закрытом issue, и мы сможем потом снова его открыть.


Видите, мне уже удалось помочь проекту Allure, даже не поменяв ни строчки кода.


Пример 2: Андрей -> Allure



Здесь чувак жалуется, что у него при запуске Allure сыпется такой-то exception, а если взять предыдущую версию Allure 2.13.5, то exception не сыпется. То есть что-то поменялось между двумя этими версиями. Это пример хорошего описания issue, потому что понятно, где искать. Я зачекаутил обе версии, сделал diff, увидел, что изменилось и понял, в чем проблема. И в итоге я ему ответил, что в Allure багов нет баги в его демопроекте. Он Allure обновил, а версию JUnit не обновил, и они друг с другом не работают. Я даже ему заслал pull request, написал, что его проблему исправит обновление JUnit в его проекте. Помог и Allure, и проекту.


Пример 3: Андрей -> Allure



А вот эта issue связана с Selenide. Также хороший баг-репорт, потому что он четко описывает, что делать, есть код для повторения.



Он говорит, что объявил PageObject, в нем объявил несколько неиспользуемых полей, а они зачем-то отображаются в отчете Allure.



Видно, что на них потрачено ноль секунд, то есть их не использовали, но в отчете эти строчки есть.


Я сначала ломанулся исправлять этот баг, но потом понял, что баг не в Allure, а в Selenide. Selenide присылает Allure такие лишние ивенты. И я ему написал, что баг в Selenide, так что заведи там баг-репорт, там будем исправлять, а в Allure можно закрывать issue.


Для мейнтейнера это большая помощь. Человеческий мозг не может удерживать в памяти сразу много вещей. Если он сегодня исправил одну-две issue или даже не исправил, а просто прошелся, посмотрел, проанализировал все, мозг отключается. Чем меньше таких отвлечений, тем лучше.


Пример 4: Андрей -> Allure


Поскольку я не нашел issue, который я мог бы полноценно исправить, я решил сделать простое исправление в Allure.


Для этого захожу в Allure, нажимаю Fork. Теперь нужно сделать git clone. Ваш URL-репозиторий есть под кнопкой Code:



Теперь идем в командную строку, вызываем git clone. Если проект не очень большой, это должно пройти довольно быстро. Заходим в папку проекта.



Советую первым делом всегда попробовать его скомпилировать. Иногда даже это не компилируется. Вы еще ничего не поменяли, а он уже не компилируется.


Лайфхак: если вы хотите попробовать сделать куда-то pull request, обновите Gradle или версию зависимости. Allure использует Gradle 6.6.1, а уже есть версия 6.7. На сайте Gradle можно подсмотреть команду, как обновить версию.



Проверяем и видим, что теперь в проекте используется Gradle 6.7. Нам нужно снова запустить тесты, чтобы убедиться, что ничего не сломалось.


Тесты запустятся, и можно сразу закоммитить. Или можно открыть этот проект в IDEA и закоммитить оттуда. Это проще и нагляднее.



Мы видим, что поменялось четыре файла, мы их закоммитим. Самый важный файл это версия Gradle.


И важный момент мы находимся в мастер-ветке. Это плохая идея коммитить в мастер-ветку. Поэтому лучше создать новую ветку с говорящим названием: upgrade-gradle-6.7. И делаем Commit and Push.



Теперь возвращаемся в репозиторий и смотрим, что случилось.



Вот важный момент это fork Allure, и находясь в нем, мне GitHub подсказывает, что появилась новая ветка и она хорошая. Он предлагает сделать из нее pull request.



GitHub сразу подсказывает с какого репозитория, с какого основного бранча ты это сделал. Здесь самая важная работа правильно написать, что вообще ты тут сделал, есть ли issue, который это описывает.


И здесь начинают прыгать статусы.



У Артёма настроены какие-то правила, которые не позволяют сразу смёрджить.


Артём: Во многих репозиториях настроены правила автоматизации. В Allure сейчас ивент перешел в Jenkins. И Jenkins поднимает Agent в Digital Ocean и запускает тестирование там. Мы потихонечку мигрируем на GitHub Actions. В GitHub Actions ты бы сразу увидел, что у тебя GitHub Action стартанул. Ну и нужен хотя бы один ревьювер, чтобы это можно было смёрджить.


Чтобы заревьюить, я захожу на свою страницу. У меня пошли проверки и мне надо дождаться CI, пока он поднимется и так далее.



Здесь провайдится jenkins-digitalocean. Сейчас Jenkins отпишется сюда о том, что билд пошел.



Заметим, что license/cla уже подписан.



Пример 5: Артём -> Selenide


Я нашел вот такой issue: не отображается URL в сообщении об ошибке. Я согласен с автором, тут есть и за, и против. Против заключается в том, что сейчас у нас все приложения одностраничные, и URL будет не настолько показательным.


Я с этим не согласен. Мне кажется, что это прекрасная идея, потому что я вижу, что здесь есть некоторый ожидаемый результат и скриншот, но не хватает информации, где я нахожусь.



Соответственно, я делаю fork. У меня два remote, обычно делаю так, что у меня есть origin remote:



То есть у меня есть ссылка на этот репозиторий, я ее всегда оставляю как origin. С большой вероятностью, если я буду долгое время контрибьютить в какой-то инструмент, например в Selenide, то мне потом дадут права, и я смогу сам создавать бранчи.


Здесь у меня origin называется eroshenkoam, и когда я буду делать бранч, я буду делать его там.



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




Я сразу же нашел места, где это используется.



Дальше смотрю, что у нас провайдится screenshotPath. Соответственно, я делаю следующее:



А теперь мне надо обработать понятие, что такое URL == null, и я здесь ставлю условную логику.



Прикольно делать комментарии, где вы указываете issue. И дальше я делаю push в свой fork. Обычно я описываю так:



И провайжу еще Action list, который будет состоять из следующих задач: provide tests, refactor meta adding. Грубо говоря, я еще провайжу список того, как вижу решение этой задачи. First draft я уже закончил. И нажимаю Draft pull request.


Соответственно, я потом пишу:



Сначала я хочу проверить, правильно я думаю или нет. И после того, как мне ответят, я запровайжу информацию о тестах и refactor meta adding.


Андрей: На Selenide pull request появился, еще пришел имейл. На Travis CI автоматически запускается билд. Он не во всем удобен, зато любой посторонний может посмотреть статус билда, какие тесты пробежали, какие упали и почему.


Сложности


Иногда сложно найти проект с исходниками, чтобы его зафоркать. У многих проектов одинаковое имя, или проект может быть не на GitHub. Например, исходники Java долгое время лежали в каком-то старом месте, их только недавно перевели на GitHub.


Бывает много forks одного и того же проекта. Первоначальный мейнтейнер уже давно ушел, вместо него уже проект поддерживает другой, затем третий. В таких случаях иногда не разобраться, в каком репозитории в итоге нужно исправлять.


Когда ты меняешь код, надо разобраться в том, какая у него структура, какие папки, в какую ветку коммитить. Бывает не только ветка master, а также ветки develop и features. Надо понимать, какую команду надо запустить локально, чтобы проверить, что ты ничего не сломал. Она тоже бывает очень разная, с кучей параметров.


В идеале нужно искать в проекте файлик CONTRIBUTING там всё это описано. К сожалению, такой файл есть не в каждом проекте. Кстати, это хорошая идея для первого pull request: если в проекте такого файла нет, создайте его сами.


И последний, самый большой вопрос, который наверняка есть у всех. Допустим, вы согласны, хотите и готовы попробовать.


Но что, если я стесняюсь? Мой код увидят, там будут косяки, я буду выглядеть глупо, меня засмеют.


Всем на самом деле все равно, плохо ли закоммитили или хорошо. Я, например, честно скажу, что в Selenide есть места, которые не идеальны, которые мне не нравятся. Даже то место, которое Артем поменял, на самом деле неудачный код. Там видно, что в одном месте много if, много ответственностей, и он ломается, если там не html. Не самый удачный код, но меня же никто не атакует.


Поэтому коммить, словно никто не видит, и пушь, словно никто не слышит. И контрибьють, словно тебя никто никогда не обижал!


Это доклад с фестиваля TechTrain, рассчитанного на самых разных IT-специалистов. Но если он вас заинтересовал, то вас могут привлечь и наши профильные IT-конференции, где контент сосредоточен на определённой области. Пока в весеннем онлайн-сезоне 2021 анонсировали пять: JPoint (Java), Heisenbug (тестирование), HolyJS (JavaScript), DotNext (.NET), Mobius (мобильная разработка).
Подробнее..

Сбор данных и отправка в Apache Kafka

15.11.2020 20:17:49 | Автор: admin

Введение


Для анализа потоковых данных необходимы источники этих данных. Так же важна сама информация, которая предоставляется источниками. А источники с текстовой информацией, к примеру, еще и редки.
Из интересных источников можно выделить следующие: twitter, vk. Но эти источники подходят не под все задачи.
Есть источники с нужными данными, но эти источники не потоковые. Здесь можно привести следующее ссылки: public-apis.
При решении задач, связанных с потоковыми данными, можно воспользоваться старым способом.
Скачать данные и отправить в поток.
Для примера можно воспользоваться следующим источником: imdb.
Следует отметить, что imdb предоставляет данные самостоятельно. См. IMDb Datasets. Но можно принять, что данные собранные напрямую содержат более актуальную информацию.


Язык: Java 1.8.
Библиотеки: kafka 2.6.0, jsoup 1.13.1.


Сбор данных


Сбор данных представляет из себя сервис, который по входным данным загружает html-страницы, ищет нужную информацию и преобразует в набор объектов.
Итак источник данных: imdb. Информация будет собираться о фильмах и будет использован следующий запрос: https://www.imdb.com/search/title/?release_date=%s,%s&countries=%s
Где 1, 2 параметр это даты. 3 параметр страны.
Для лучшего понимания источника данных можно обратится к следующему ресурсу: imdb-extensive-dataset.


Интерфейс для сервиса:


public interface MovieDirectScrapingService {    Collection<Movie> scrap();}

Класс Movie это класс, которые содержит информацию об одном фильме (или о шоу и т.п.).


class Movie {    public final String titleId;    public final String titleUrl;    public final String title;    public final String description;    public final Double rating;    public final String genres;    public final String runtime;    public final String baseUrl;    public final String baseNameUrl;    public final String baseTitleUrl;    public final String participantIds;    public final String participantNames;    public final String directorIds;    public final String directorNames;

Анализ данных на одной странице.
Информация собирается следующим образом. Данные закачиваются с помощью jsoup. Далее ищутся нужные html-элементы и трансформируются в экземпляры для фильмов.


String scrap(String url, List<Movie> items) {    Document doc = null;    try {        doc = Jsoup.connect(url).header("Accept-Language", language).get();    } catch (IOException e) {        e.printStackTrace();    }    if (doc != null) {        collectItems(doc, items);        return nextUrl(doc);    }    return "";}

Поиск ссылки на следующею страницу.


String nextUrl(Document doc) {    Elements nextPageElements = doc.select(".next-page");    if (nextPageElements.size() > 0) {        Element hrefElement = nextPageElements.get(0);        return baseUrl + hrefElement.attributes().get("href");    }    return "";}

Тогда основной метод будет таким. Формируется начальная строка поиска. Закачиваются данные по одной странице. Если есть следующая страница, то идет переход к ней. По окончании передаются накопленные данные.


@Overridepublic Collection<Movie> scrap() {    String url = String.format(            baseUrl + "/search/title/?release_date=%s,%s&countries=%s",            startDate, endDate, countries    );    List<Movie> items = new ArrayList<>();    String nextUrl = url;    while (true) {        nextUrl = scrap(nextUrl, items);        if ("".equals(nextUrl)) {            break;        }        try {            Thread.sleep(50);        } catch (InterruptedException e) {        }    }    return items;}

Подробности по остальным методам можно найти в ссылках на ресурсы.


Отправка данных в топик


Формируется следующий сервис: MovieProducer. Здесь будет один единственный публичный метод: run.


Создается продюсер для кафки. Загружаются данные из источника. Трансформируются и отправляются в топик.


public void run() {    try (SimpleStringStringProducer producer = new SimpleStringStringProducer(            bootstrapServers, clientId, topic)) {        Collection<Data.Movie> movies = movieDirectScrapingService.scrap();        List<SimpleStringStringProducer.KeyValueStringString> kvList = new ArrayList<>();        for (Data.Movie move : movies) {            Map<String, String> map = new HashMap<>();            map.put("title_id", move.titleId);            map.put("title_url", move.titleUrl);                        String value = JSONObject.toJSONString(map);            String key = UUID.randomUUID().toString();            kvList.add(new SimpleStringStringProducer.KeyValueStringString(key, value));        }        producer.produce(kvList);    }}

Теперь все вместе


Формируются нужные параметры для поиска. Загружаются данные и отправляются в топик.
Для этого понадобится еще один класс: MovieDirectScrapingExecutor. С одним публичным методом: run.


В цикле создаются данные для поиска из текущей даты. Происходит загрузка и отправка данных в топик.


public void run() {    int countriesCounter = 0;    List<String> countriesSource = Arrays.asList("us");    while (true) {        try {            LocalDate localDate = LocalDate.now();            int year = localDate.getYear();            int month = localDate.getMonthValue();            int day = localDate.getDayOfMonth();            String monthString = month < 9 ? "0" + month : Integer.toString(month);            String dayString = day < 9 ? "0" + day : Integer.toString(day);            String startDate = year + "-" + monthString + "-" + dayString;            String endDate = startDate;            String language = "en";            String countries = countriesSource.get(countriesCounter);            execute(language, startDate, endDate, countries);            Thread.sleep(1000);            countriesCounter += 1;            if (countriesCounter >= countriesSource.size()) {                countriesCounter = 0;            }        } catch (InterruptedException e) {        }    }}

Для запуска потребуется экземпляр класса MovieDirectScrapingExecutor, который можно запустить с нужными параметрами, к примеру, из метода main.


Пример отправляемых данных для одного фильма.


{  "base_name_url": "https:\/\/www.imdb.com\/name",  "participant_ids": "nm7947173~nm2373827~nm0005288~nm0942193~",  "title_id": "tt13121702",  "rating": "0.0",  "base_url": "https:\/\/www.imdb.com",  "description": "It's Christmas time and Jackie (Carly Hughes), an up-and-coming journalist, finds that her life is at a crossroads until she finds an unexpected opportunity - to run a small-town newspaper ... See full summary ",  "runtime": "",  "title": "The Christmas Edition",  "director_ids": "nm0838289~",  "title_url": "\/title\/tt13121702\/?ref_=adv_li_tt",  "director_names": "Peter Sullivan~",  "genres": "Drama, Romance",  "base_title_url": "https:\/\/www.imdb.com\/title",  "participant_names": "Carly Hughes~Rob Mayes~Marie Osmond~Aloma Wright~"}

Подробности можно найти в ссылках на ресурсы.


Тесты


Для тестирования основной логики, которая связана с отправкой данных, можно воспользоваться юнит-тестами. В тестах предварительно создается kafka-сервер.
См. Apache Kafka и тестирование с Kafka Server.


Сам тест: MovieProducerTest.


public class MovieProducerTest {    @Test    void simple() throws InterruptedException {        String brokerHost = "127.0.0.1";        int brokerPort = 29092;        String zooKeeperHost = "127.0.0.1";        int zooKeeperPort = 22183;        String bootstrapServers = brokerHost + ":" + brokerPort;        String topic = "q-data";        String clientId = "simple";        try (KafkaServerService kafkaServerService = new KafkaServerService(                brokerHost, brokerPort, zooKeeperHost, zooKeeperPort        )        ) {            kafkaServerService.start();            kafkaServerService.createTopic(topic);            MovieDirectScrapingService movieDirectScrapingServiceImpl = () -> Collections.singleton(                    new Data.Movie()            );            MovieProducer movieProducer =                    new MovieProducer(bootstrapServers, clientId, topic, movieDirectScrapingServiceImpl);            movieProducer.run();            kafkaServerService.poll(topic, "simple", 1, 5, (records) -> {                assertTrue(records.count() > 0);                ConsumerRecord<String, String> record = records.iterator().next();                JSONParser jsonParser = new JSONParser();                JSONObject jsonObject = null;                try {                    jsonObject = (JSONObject) jsonParser.parse(record.value());                } catch (ParseException e) {                    e.printStackTrace();                }                assertNotNull(jsonObject);                    });            Thread.sleep(5000);        }    }}

Заключение


Конечно, описанный здесь способ получения источника потоковых данных, строго потоковым не является. Но для исследований и прототипов вполне может сойти.


Ссылки и ресурсы


Исходный код.

Подробнее..

Сервисы с Apache Kafka и тестирование

09.01.2021 18:16:01 | Автор: admin

Когда сервисы интегрируются при помощи Kafka очень удобно использовать REST API, как универсальный и стандартный способ обмена сообщениями. При увеличении количества сервисов сложность коммуникаций увеличивается. Для контроля можно и нужно использовать интеграционное тестирование. Такие библиотеки как testcontainers или EmbeddedServer прекрасно помогают организовать такое тестирование. Существуют много примеров для micronaut, Spring Boot и т.д. Но в этих примерах опущены некоторые детали, которые не позволяют с первого раза запустить код. В статье приводятся примеры с подробным описанием и ссылками на код.


Пример


Для простоты можно принять такой REST API.


/runs POST-метод. Инициализирует запрос в канал связи. Принимает данные и возвращает ключ запроса.
/runs/{key}/status GET-метод. По ключу возвращает статус запроса. Может принимать следующие значения: UNKNOWN, RUNNING, DONE.
/runs /{key} GET-метод. По ключу возвращает результат запроса.


Подобный API реализован у livy, хотя и для других задач.


Реализация


Будут использоваться: micronaut, Spring Boot.


micronaut


Контроллер для API.


import io.micronaut.http.annotation.Body;import io.micronaut.http.annotation.Controller;import io.micronaut.http.annotation.Get;import io.micronaut.http.annotation.Post;import io.reactivex.Maybe;import io.reactivex.schedulers.Schedulers;import javax.inject.Inject;import java.util.UUID;@Controller("/runs")public class RunController {    @Inject    RunClient runClient;    @Inject    RunCache runCache;    @Post    public String runs(@Body String body) {        String key = UUID.randomUUID().toString();        runCache.statuses.put(key, RunStatus.RUNNING);        runCache.responses.put(key, "");        runClient.sendRun(key, new Run(key, RunType.REQUEST, "", body));        return key;    }    @Get("/{key}/status")    public Maybe<RunStatus> getRunStatus(String key) {        return Maybe.just(key)                .subscribeOn(Schedulers.io())                .map(it -> runCache.statuses.getOrDefault(it, RunStatus.UNKNOWN));    }    @Get("/{key}")    public Maybe<String> getRunResponse(String key) {        return Maybe.just(key)                .subscribeOn(Schedulers.io())                .map(it -> runCache.responses.getOrDefault(it, ""));    }}

Отправка сообщений в kafka.


import io.micronaut.configuration.kafka.annotation.*;import io.micronaut.messaging.annotation.Body;@KafkaClientpublic interface RunClient {    @Topic("runs")    void sendRun(@KafkaKey String key, @Body Run run);}

Получение сообщений из kafka.


import io.micronaut.configuration.kafka.annotation.*;import io.micronaut.messaging.annotation.Body;import javax.inject.Inject;@KafkaListener(offsetReset = OffsetReset.EARLIEST)public class RunListener {    @Inject    RunCalculator runCalculator;    @Topic("runs")    public void receive(@KafkaKey String key, @Body Run run) {        runCalculator.run(key, run);    }}

Обработка сообщений происходит в RunCalculator. Для тестов используется особая реализация, в которой происходит переброска сообщений.


import io.micronaut.context.annotation.Replaces;import javax.inject.Inject;import javax.inject.Singleton;import java.util.UUID;@Replaces(RunCalculatorImpl.class)@Singletonpublic class RunCalculatorWithWork implements RunCalculator {    @Inject    RunClient runClient;    @Inject    RunCache runCache;    @Override    public void run(String key, Run run) {        if (RunType.REQUEST.equals(run.getType())) {            String runKey = run.getKey();            String newKey = UUID.randomUUID().toString();            String runBody = run.getBody();            runClient.sendRun(newKey, new Run(newKey, RunType.RESPONSE, runKey, runBody + "_calculated"));        } else if (RunType.RESPONSE.equals(run.getType())) {            runCache.statuses.replace(run.getResponseKey(), RunStatus.DONE);            runCache.responses.replace(run.getResponseKey(), run.getBody());        }    }}

Тест.


import io.micronaut.http.HttpRequest;import io.micronaut.http.client.HttpClient;import static org.junit.jupiter.api.Assertions.assertEquals;public abstract class RunBase {    void run(HttpClient client) {        String key = client.toBlocking().retrieve(HttpRequest.POST("/runs", "body"));        RunStatus runStatus = RunStatus.UNKNOWN;        while (runStatus != RunStatus.DONE) {            runStatus = client.toBlocking().retrieve(HttpRequest.GET("/runs/" + key + "/status"), RunStatus.class);            try {                Thread.sleep(500);            } catch (InterruptedException e) {                e.printStackTrace();            }        }        String response = client.toBlocking().retrieve(HttpRequest.GET("/runs/" + key), String.class);        assertEquals("body_calculated", response);    }}

Для использования EmbeddedServer необходимо.


Подключить библиотеки:


testImplementation("org.apache.kafka:kafka-clients:2.6.0:test")testImplementation("org.apache.kafka:kafka_2.12:2.6.0")testImplementation("org.apache.kafka:kafka_2.12:2.6.0:test")

Тест может выглядеть так.


import io.micronaut.context.ApplicationContext;import io.micronaut.http.client.HttpClient;import io.micronaut.runtime.server.EmbeddedServer;import org.junit.jupiter.api.Test;import java.util.HashMap;import java.util.Map;public class RunKeTest extends RunBase {    @Test    void test() {        Map<String, Object> properties = new HashMap<>();        properties.put("kafka.bootstrap.servers", "localhost:9092");        properties.put("kafka.embedded.enabled", "true");        try (EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer.class, properties)) {            ApplicationContext applicationContext = embeddedServer.getApplicationContext();            HttpClient client = applicationContext.createBean(HttpClient.class, embeddedServer.getURI());            run(client);        }    }}

Для использования testcontainers необходимо.


Подключить библиотеки:


implementation("org.testcontainers:kafka:1.14.3")

Тест может выглядеть так.


import io.micronaut.context.ApplicationContext;import io.micronaut.http.client.HttpClient;import io.micronaut.runtime.server.EmbeddedServer;import org.junit.jupiter.api.Test;import org.testcontainers.containers.KafkaContainer;import org.testcontainers.utility.DockerImageName;import java.util.HashMap;import java.util.Map;public class RunTcTest extends RunBase {    @Test    public void test() {        try (KafkaContainer kafka = new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:5.5.3"))) {            kafka.start();            Map<String, Object> properties = new HashMap<>();            properties.put("kafka.bootstrap.servers", kafka.getBootstrapServers());            try (EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer.class, properties)) {                ApplicationContext applicationContext = embeddedServer.getApplicationContext();                HttpClient client = applicationContext.createBean(HttpClient.class, embeddedServer.getURI());                run(client);            }        }    }}

Spring Boot


Контроллер для API.


import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.*;import java.util.UUID;@RestController@RequestMapping("/runs")public class RunController {    @Autowired    private RunClient runClient;    @Autowired    private RunCache runCache;    @PostMapping()    public String runs(@RequestBody String body) {        String key = UUID.randomUUID().toString();        runCache.statuses.put(key, RunStatus.RUNNING);        runCache.responses.put(key, "");        runClient.sendRun(key, new Run(key, RunType.REQUEST, "", body));        return key;    }    @GetMapping("/{key}/status")    public RunStatus getRunStatus(@PathVariable String key) {        return runCache.statuses.getOrDefault(key, RunStatus.UNKNOWN);    }    @GetMapping("/{key}")    public String getRunResponse(@PathVariable String key) {        return runCache.responses.getOrDefault(key, "");    }}

Отправка сообщений в kafka.


import com.fasterxml.jackson.core.JsonProcessingException;import com.fasterxml.jackson.databind.ObjectMapper;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.kafka.core.KafkaTemplate;import org.springframework.stereotype.Component;@Componentpublic class RunClient {    @Autowired    private KafkaTemplate<String, String> kafkaTemplate;    @Autowired    private ObjectMapper objectMapper;    public void sendRun(String key, Run run) {        String data = "";        try {            data = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(run);        } catch (JsonProcessingException e) {            e.printStackTrace();        }        kafkaTemplate.send("runs", key, data);    }}

Получение сообщений из kafka.


import com.fasterxml.jackson.core.JsonProcessingException;import com.fasterxml.jackson.databind.ObjectMapper;import org.apache.kafka.clients.consumer.ConsumerRecord;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.kafka.annotation.KafkaListener;import org.springframework.stereotype.Component;@Componentpublic class RunListener {    @Autowired    private ObjectMapper objectMapper;    @Autowired    private RunCalculator runCalculator;    @KafkaListener(topics = "runs", groupId = "m-group")    public void receive(ConsumerRecord<?, ?> consumerRecord) {        String key = consumerRecord.key().toString();        Run run = null;        try {            run = objectMapper.readValue(consumerRecord.value().toString(), Run.class);        } catch (JsonProcessingException e) {            e.printStackTrace();        }        runCalculator.run(key, run);    }}

Обработка сообщений происходит в RunCalculator. Для тестов используется особая реализация, в которой происходит переброска сообщений.


import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Component;import java.util.UUID;@Componentpublic class RunCalculatorWithWork implements RunCalculator {    @Autowired    RunClient runClient;    @Autowired    RunCache runCache;    @Override    public void run(String key, Run run) {        if (RunType.REQUEST.equals(run.getType())) {            String runKey = run.getKey();            String newKey = UUID.randomUUID().toString();            String runBody = run.getBody();            runClient.sendRun(newKey, new Run(newKey, RunType.RESPONSE, runKey, runBody + "_calculated"));        } else if (RunType.RESPONSE.equals(run.getType())) {            runCache.statuses.replace(run.getResponseKey(), RunStatus.DONE);            runCache.responses.replace(run.getResponseKey(), run.getBody());        }    }}

Тест.


import com.fasterxml.jackson.databind.ObjectMapper;import org.springframework.http.MediaType;import org.springframework.test.web.servlet.MockMvc;import org.springframework.test.web.servlet.MvcResult;import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;import static org.junit.jupiter.api.Assertions.assertEquals;import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;public abstract class RunBase {    void run(MockMvc mockMvc, ObjectMapper objectMapper) throws Exception {        MvcResult keyResult = mockMvc.perform(MockMvcRequestBuilders.post("/runs")                .content("body")                .contentType(MediaType.APPLICATION_JSON)                .accept(MediaType.APPLICATION_JSON))                .andExpect(status().isOk())                .andReturn();        String key = keyResult.getResponse().getContentAsString();        RunStatus runStatus = RunStatus.UNKNOWN;        while (runStatus != RunStatus.DONE) {            MvcResult statusResult = mockMvc.perform(MockMvcRequestBuilders.get("/runs/" + key + "/status")                    .contentType(MediaType.APPLICATION_JSON)                    .accept(MediaType.APPLICATION_JSON))                    .andExpect(status().isOk())                    .andReturn();            runStatus = objectMapper.readValue(statusResult.getResponse().getContentAsString(), RunStatus.class);            try {                Thread.sleep(500);            } catch (InterruptedException e) {                e.printStackTrace();            }        }        String response = mockMvc.perform(MockMvcRequestBuilders.get("/runs/" + key)                .contentType(MediaType.APPLICATION_JSON)                .accept(MediaType.APPLICATION_JSON))                .andExpect(status().isOk())                .andReturn().getResponse().getContentAsString();        assertEquals("body_calculated", response);    }}

Для использования EmbeddedServer необходимо.


Подключить библиотеки:


<dependency>    <groupId>org.springframework.kafka</groupId>    <artifactId>spring-kafka</artifactId>    <version>2.5.10.RELEASE</version></dependency><dependency>    <groupId>org.springframework.kafka</groupId>    <artifactId>spring-kafka-test</artifactId>    <version>2.5.10.RELEASE</version>    <scope>test</scope></dependency>

Тест может выглядеть так.


import com.fasterxml.jackson.databind.ObjectMapper;import org.junit.jupiter.api.Test;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;import org.springframework.boot.test.context.SpringBootTest;import org.springframework.boot.test.context.TestConfiguration;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Import;import org.springframework.kafka.test.context.EmbeddedKafka;import org.springframework.test.web.servlet.MockMvc;@AutoConfigureMockMvc@SpringBootTest@EmbeddedKafka(partitions = 1, brokerProperties = {"listeners=PLAINTEXT://localhost:9092", "port=9092"})@Import(RunKeTest.RunKeTestConfiguration.class)public class RunKeTest extends RunBase {    @Autowired    private MockMvc mockMvc;    @Autowired    private ObjectMapper objectMapper;    @Test    void test() throws Exception {        run(mockMvc, objectMapper);    }    @TestConfiguration    static class RunKeTestConfiguration {        @Autowired        private RunCache runCache;        @Autowired        private RunClient runClient;        @Bean        public RunCalculator runCalculator() {            RunCalculatorWithWork runCalculatorWithWork = new RunCalculatorWithWork();            runCalculatorWithWork.runCache = runCache;            runCalculatorWithWork.runClient = runClient;            return runCalculatorWithWork;        }    }}

Для использования testcontainers необходимо.


Подключить библиотеки:


<dependency>    <groupId>org.testcontainers</groupId>    <artifactId>kafka</artifactId>    <version>1.14.3</version>    <scope>test</scope></dependency>

Тест может выглядеть так.


import com.fasterxml.jackson.databind.ObjectMapper;import org.apache.kafka.clients.consumer.ConsumerConfig;import org.apache.kafka.clients.producer.ProducerConfig;import org.apache.kafka.common.serialization.StringDeserializer;import org.apache.kafka.common.serialization.StringSerializer;import org.junit.ClassRule;import org.junit.jupiter.api.Test;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;import org.springframework.boot.test.context.SpringBootTest;import org.springframework.boot.test.context.TestConfiguration;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Import;import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory;import org.springframework.kafka.core.*;import org.springframework.test.web.servlet.MockMvc;import org.testcontainers.containers.KafkaContainer;import org.testcontainers.utility.DockerImageName;import java.util.HashMap;import java.util.Map;@AutoConfigureMockMvc@SpringBootTest@Import(RunTcTest.RunTcTestConfiguration.class)public class RunTcTest extends RunBase {    @ClassRule    public static KafkaContainer kafka = new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:5.5.3"));    static {        kafka.start();    }    @Autowired    private MockMvc mockMvc;    @Autowired    private ObjectMapper objectMapper;    @Test    void test() throws Exception {        run(mockMvc, objectMapper);    }    @TestConfiguration    static class RunTcTestConfiguration {        @Autowired        private RunCache runCache;        @Autowired        private RunClient runClient;        @Bean        ConcurrentKafkaListenerContainerFactory<Integer, String> kafkaListenerContainerFactory() {            ConcurrentKafkaListenerContainerFactory<Integer, String> factory = new ConcurrentKafkaListenerContainerFactory<>();            factory.setConsumerFactory(consumerFactory());            return factory;        }        @Bean        public ConsumerFactory<Integer, String> consumerFactory() {            return new DefaultKafkaConsumerFactory<>(consumerConfigs());        }        @Bean        public Map<String, Object> consumerConfigs() {            Map<String, Object> props = new HashMap<>();            props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, kafka.getBootstrapServers());            props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");            props.put(ConsumerConfig.GROUP_ID_CONFIG, "m-group");            props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);            props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);            return props;        }        @Bean        public ProducerFactory<String, String> producerFactory() {            Map<String, Object> configProps = new HashMap<>();            configProps.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, kafka.getBootstrapServers());            configProps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);            configProps.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);            return new DefaultKafkaProducerFactory<>(configProps);        }        @Bean        public KafkaTemplate<String, String> kafkaTemplate() {            return new KafkaTemplate<>(producerFactory());        }        @Bean        public RunCalculator runCalculator() {            RunCalculatorWithWork runCalculatorWithWork = new RunCalculatorWithWork();            runCalculatorWithWork.runCache = runCache;            runCalculatorWithWork.runClient = runClient;            return runCalculatorWithWork;        }    }}

Перед всеми тестами необходимо стартовать kafka. Это делается вот таким вот образом:


kafka.start();

Дополнительные свойства для kafka в тестах можно задать в ресурсном файле.


application.yml

spring:  kafka:    consumer:      auto-offset-reset: earliest

Ресурсы и ссылки


Код для micronaut


Код для Spring Boot


PART 1: TESTING KAFKA MICROSERVICES WITH MICRONAUT


Testing Kafka and Spring Boot


Micronaut Kafka


Spring for Apache Kafka

Подробнее..

ZERG что за зверь?

19.02.2021 14:12:20 | Автор: admin


Когда мы говорим о CI&CD, мы часто углубляемся в базовые инструменты автоматизации сборки, тестирования и доставки приложения фокусируемся на инструментах, но забываем осветить процессы, которые протекают во время отрезания и стабилизации релизов. Однако, не все готовые инструменты одинаково полезны, а какие-то кастомные процессы не укладываются в их покрытие. Приходится исследовать процессы и находить пути автоматизации для их оптимизации.

В нашей компании QA-инженеры используют Zephyr для отслеживания хода регресса, так как ручное и исследовательское тестирование нам не заменить автотестами. Но несмотря на это, автотесты у нас гоняются часто и в больших количествах, поэтому хочется иметь возможность опустить какие-то банальные проверки, которые были автоматизированы и дать тестировщикам заниматься более производительной и полезной работой.

У нас есть ночные прогоны, когда гоняются полные наборы тестов. Но на самой заре освоения Zephyr, нашим тестировщикам во время регресса приходилось скачивать xcresult, или ещё ранее plist, или junit xml, а затем проставлять соответствия зелёных и красных тестов в зефире руками. Это довольно рутинная операция, да и занимает она много времени, чтобы руками пройти 500-600 тестов. Такие вещи хочется отдать на откуп бездушной машине. Так родился ZERG.


Рождение зерга


Zephyr Enterprise Report Generator небольшая утилита, которая изначально умела только искать соответствия в отчёте тестов и отправлять в Zephyr их актуальные статусы. Позже утилита получила новые функции, но сегодня мы остановимся на поиске и отправке отчётов.
В Zephyr нам предлагается оперировать версиями, циклами и проходами (execution) тест кейсов. Каждая версия содержит произвольное количество циклов, а каждый цикл содержит в себе проходы кейсов. Такие проходы содержат в себе информацию о задаче (zephyr прекрасно интегрируется с jira и тест кейс это, по сути, задачка в jira), авторе, о статусе кейса, а также о том, кто занимается этим кейсом и о других необходимых деталях.
Для автоматизации проблемы, которую мы обозначили выше, нам важно разобраться в проставлении статуса кейса.

Работа с кодом


Но как соотнести тест в коде и тест в зефире? Здесь мы выбрали довольно простой и прямолинейный подход: для каждого теста мы добавляем в секцию комментариев ссылки на задачи в jira.


В комментариях также могут размещаться дополнительные параметры, но об этом позже.
Так, один тест в коде может покрывать несколько задач. Но работает и обратная логика. Для одной задачи может быть написано несколько тестов в коде. Их статусы будут учитываться при составлении отчёта.
Нам надо пройти по исходникам, извлечь все тест-классы и тесты, слинковать задачи с методами и соотнести это с отчётом о прохождении тестов ( xcresult или junit ).
Работать с самим кодом можно разными путями:
просто читать файлы и через регулярные выражения извлекать информацию
использовать SourceKit

Как бы то ни было, даже при использовании SourceKit мы не обойдёмся без регулярных выражений для извлечения идентификаторов задач из ссылок в комментариях.
На данном этапе нас не интересуют детали, поэтому отгородимся от них протоколом:



Нам надо получить тесты. Для этого опишем структуры:



Затем нам надо прочитать отчёт о прохождении тестов. ZERG был рождён ещё до переезда на xcresult, и поэтому умеет парсить plist и junit. Детали в этой статье нас всё ещё не интересуют, они будут приложены в коде. Поэтому отгородимся протоколам



Остается только слинковать тесты в коде с результатами тестов из отчетов.



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

Работаем с зефиром


Теперь, когда мы прочитали отчёты о тестировании, нам надо их перевести в контекст zephyr. Для этого надо получить список версий проекта, соотнести с версией приложения (чтобы это так работало, необходимо, чтобы версия в зефире совпадала с версией в Info.plist вашего приложения, например, 2.56), выкачать циклы и проходы. А дальше соотнести проходы с нашими уже имеющимися отчётами.
Для этого нам надо реализовать в ZephyrAPI следующие методы:



Cпецификацию можно увидеть здесь: getzephyr.docs.apiary.io, а реализацию клиента в нашем репозитории.
Общий алгоритм довольно простой:



На этапе сопоставления проходов с отчётами есть тонкий момент, который необходимо учитывать: в zephyr api обновление execution отправлять удобнее всего пачками, где передаётся общий статус и список идентификаторов проходов. Нам нужно развернуть наши отчёты относительно тикетов и учесть n-m соотношение. Для одного кейса в зефире может быть несколько тестов в коде. Один тест в коде может покрывать несколько кейсов. Если для одного кейса есть n тестов в коде и один из них красный, то для такого кейса общий статус красный, однако если один из таких тестов покрывает m кейсов и он зелёный, то остальные кейсы не должны стать красными.
Поэтому мы оперируем сетами и ищем пересечение красных и зелёных. Всё, что попадает в пересечение, мы отнимаем из зелёных результатов и отправляем отредактированные сведения в zephyr.



Здесь ещё нужно отметить, что внутри команды мы договорились, что zerg не будет менять статус прохода, если:
1. Текущий статус blocked или failed (раньше для failed мы меняли статус, но сейчас отказались от практики, потому что хотим, чтобы тестировщики обращали внимание на красные автотесты во время регресса).
2. Если текущий статус pass и его поставил человек, а не zerg.
3. Если тест помечен как флакающий.

Интересности Zephyr API


При запросе циклов нам приходит json, который с первого взгляда невозможно систематизировать для десериализации. Всё дело в том, что запрос на получение циклов для версии, хотя по смыслу и должен возвращать массив циклов, на самом деле возвращает один объект, где каждое поле уникально и называется идентификатором цикла, который лежит в значении. Чтобы такое обработать, мы применяем несложные хаки:



Статусы прохождения тестов приходят в одном из запросов рядом с объектом запроса. Но их можно вынести заранее в enum:



При запросе циклов нужно передавать в параметры запроса версию и целочисленный идентификатор проекта. Но в запросе проходов для цикла надо этот же идентификатор проекта передавать в формате строки.



Вместо заключения


Если есть много рутинной работы, то, скорее всего, что-то можно автоматизировать. Синхронизация прохождения автотестов с системой ведения тестов один из таких кейсов.
Таким образом, каждую ночь у нас проходит полный прогон тестов, а во время регресса отчёт отправляется QA-инженерам. Это снижает время регресса и даёт время на исследовательское тестирование.
Если правильно реализовать парсер андроид-исходников, то его можно с тем же успехом применять и для второй платформы.
Наш Zerg помимо сопоставления тестов ещё умеет в начальный импакт анализ, но об этом, возможно, в следующий раз.
Подробнее..

Fast-Unit или декларативный подход к юнит-тестам

03.09.2020 16:16:34 | Автор: admin

Всем привет! Меня зовут Юрий Скворцов, наша команда занимается автоматизированным тестированием в Росбанке. Одной из наших задач является разработка средств для автоматизации функционального тестирования.

В этой статье я хочу рассказать о решении, которое задумывалось как небольшая вспомогательная утилита для решения прочих задач, а в итоге превратилось в самостоятельный инструмент. Речь пойдет о фреймворке Fast-Unit, который позволяет писать юнит-тесты в декларативном стиле и превращает разработку юнит-тестов в конструктор компонентов. Проект разрабатывался в первую очередь для тестирования нашего основного продукта -Tladianta единого BDD-фреймворка для тестирования 4-х платформ: Desktop, Web, Mobile и Rest.

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

На первом этапе мы попытались воспользоваться готовыми инструментами, такими как assertJ и Mockito, но быстро столкнулись с некоторыми техническими особенностями нашего проекта:

  • Tladianta уже использует JUnit4 как зависимость, что делает сложным использование другой версии JUnit и усложняет работу с Before;
  • Tladianta содержит компоненты для работы с различными платформами, в ней есть множество сущностей крайне близких с точки зрения функционала, но с разной иерархией и разным поведением;
  • многие компоненты требуют значительной предварительной настройки и могут фонить (влиять на результаты других тестов) при массовом прогоне;
  • некоторые компоненты фреймворка используют другие активно развивающиеся зависимости, и, к сожалению, мы далеко не всегда можем положиться на их надежность и гарантии обратной совместимости, а значит проверять нужно и их работу;
  • некоторую функциональность фреймворка приходится полностью блокировать при запуске юнит-тестов (например, запуск Appium во время тестов крайне нежелателен, тем более, что он закончится ошибкой, поскольку сервер для него никто не поднимал);
  • необходимость толстых обвязок, а также реализации ожиданий: Mockito тут нам помочь полноценно не смог.

Первый подход к снаряду


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

@Testpublic void checkOpenHint() {    ElementManager.getInstance().register(xpath,ElementManager.Condition.VISIBLE,ElementManager.Condition.DISABLED);    new HintStepDefs().open(("Подсказка");    assertTrue(TestResults.getInstance().isSuccessful("Open"));    assertTrue(TestResults.getInstance().isSuccessful("Click"));}@Testpublic void checkCloseHint() {    ElementManager.getInstance().register(xpath);    new HintStepDefs().close("Подсказка");    assertTrue(TestResults.getInstance().isSuccessful("Close"));    assertTrue(TestResults.getInstance().isSuccessful("Click"));}

Или вообще вот так:

@Testpublic void fillFieldsTestOld() {    ElementManager.getInstance().register(ElementManager.Type.CHECK_BOX,"//check-box","",ElementManager.Condition.NOT_SELECTED);        ElementManager.getInstance().register(ElementManager.Type.INPUT,"//input","");        ElementManager.getInstance().register(ElementManager.Type.RADIO_GROUP, "//radio-group","");        DataTable dataTable = new Cucumber.DataTableBuilder()                .withRow("Чекбокс", "true")                .withRow("Радио", "not selected element")                .withRow("Текстовое поле", "text")                .build();        new HtmlCommonSteps().fillFields(dataTable);        assertEquals(TestResults.getInstance().getTestResult("set"), ElementProvider.getInstance().provide("//check-box").force().getAttribute("test-id"));        assertEqualsTestResults.getInstance().getTestResult("sendKeys"), ElementProvider.getInstance().provide("//input").force().getAttribute("test-id"));        assertEquals(TestResults.getInstance().getTestResult("selectByValue"), ElementProvider.getInstance().provide("//radio-group").force().getAttribute("test-id"));    }

В коде выше найти то, что именно тестируется, несложно, как и разобраться в проверках, но кода огромное количество. Если включить сюда софт проверки и описания ошибок, то читать станет очень сложно. А мы всего лишь пытаемся проверить, что метод вызвался у нужного объекта, при том что реальная логика проверок крайне примитивна. Для того чтобы написать такой тест, надо знать про ElementManager, ElementProvider, TestResults, TickingFuture (обертка, чтобы реализовать изменение состояния элемента в течении заданного времени). Эти компоненты отличались в разных проектах, мы не успевали синхронизировать изменения.

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

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

Меняем философию


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

@IExpectTestResult(errDesc = "Не был вызван метод set", value = "set",expected = "//check-box", convertedBy = Converters.XpathToIdConverter.class, soft = true)@IExpectTestResult(errDesc = "Не был вызван метод sendKeys", value = "sendKeys", expected = "//input", convertedBy = Converters.XpathToIdConverter.class, soft = true)@IExpectTestResult(errDesc = "Не был вызван метод selectByValue", value = "selectByValue",expected = "//radio-group", convertedBy = Converters.XpathToIdConverter.class, soft = true)@Testpublic void fillFieldsTestOld() {    ElementManager.getInstance().register(ElementManager.Type.CHECK_BOX, "//check-box", "",ElementManager.Condition.NOT_SELECTED);    ElementManager.getInstance().register(ElementManager.Type.INPUT, "//input", "");    ElementManager.getInstance().register(ElementManager.Type.RADIO_GROUP, "//radio-group", "");    DataTable dataTable = new Cucumber.DataTableBuilder()            .withRow("Чекбокс", "true")            .withRow("Радио", "not selected element")            .withRow("Текстовое поле", "text")            .build();    runTest("fillFields", dataTable);}

Что изменилось?

  • Проверки были делегированы отдельному компоненту. Теперь не надо знать о том, как хранятся элементы, результаты тестов.
  • Что не менее важно: errDesc является обязательным полем, так что теперь даже не знакомый с кодом человек может сразу увидеть суть проверки.
  • Исчез конструктор, а определить при ревью, что именно проверяется, стало проще это всегда один и тот же метод runTest, теперь уже не требуется вникать в код, чтобы понять какой метод проверяется.
  • Невозможно перепутать ожидаемое и реальное значение.
  • Софт-проверки теперь определяются просто параметром аннотации, так что использовать их становится гораздо удобнее.

Такая форма записи нам понравилась, и мы решили таким же образом упростить другой сложный компонент генерацию элементов. Большая часть наших тестов посвящена готовым степам, и мы должны быть уверены, что они корректно работают, однако для таких проверок необходимо полностью запустить фейковое приложение и наполнить его элементами (напомним, что речь идет про Web, Desktop и Mobile, инструменты для которых отличаются достаточно сильно).

@IGenerateElement(type = ElementManager.Type.CHECK_BOX)@IGenerateElement(type = ElementManager.Type.RADIO_GROUP)@IGenerateElement(type = ElementManager.Type.INPUT)@Test@IExpectTestResult(errDesc = "Не был вызван метод set", value = "set", expected = "//check-box", convertedBy = Converters.XpathToIdConverter.class, soft = true)@IExpectTestResult(errDesc = "Не был вызван метод sendKeys", value = "sendKeys", expected = "//input", convertedBy = Converters.XpathToIdConverter.class, soft = true)@IExpectTestResult(errDesc = "Не был вызван метод selectByValue", value = "selectByValue",expected = "//radio-group", convertedBy = Converters.XpathToIdConverter.class, soft = true)public void fillFieldsTest() {    DataTable dataTable = new Cucumber.DataTableBuilder()            .withRow("Чекбокс", "true")            .withRow("Радио", "not selected element")            .withRow("Текстовое поле", "text")            .build();    runTest("fillFields", dataTable);}

Теперь код теста превратился в полностью шаблонный, параметры явно видны, а вся логика вынесена в шаблонные компоненты. Дефолтные свойства позволили убрать пустые строки и дали широкие возможности для перегрузок. Этот код почти соответствует BDD-подходу, предусловие, проверка, действие. К тому же, из логики тестов вылетели все обвязки, больше не нужно знать про менеджеры, хранилища тестовых результатов, код прост и легко читаем. Поскольку аннотации в Java почти не кастомизируются, мы ввели механизм конвертеров, которые из строки могут получать итоговый результат. Этот код не только проверяет сам факт вызова метода, но и id элемента, который его выполнил. Почти все существовавшие на тот момент тесты (более 200 единиц) мы достаточно быстро перевели на эту логику, приведя их к единому шаблону. Тесты стали тем, чем они должны быть документацией, а не кодом, так мы пришли к декларативности. Именно этот подход лег в основу Fast-Unit декларативность, самодокументируемость тестов и изоляция тестируемого функционала, тест полностью посвящен проверке одного тестируемого метода.

Продолжаем развитие


Теперь требовалось добавить возможность создавать такие компоненты самостоятельно в рамках проектов, добавить возможность управлять последовательностью их срабатывания. Для этого мы разработали концепцию фаз: в отличие от Junit, все эти фазы существуют независимо в рамках каждого теста и исполняются в момент выполнения теста. В качестве дефолтной реализации мы заложили такой жизненный цикл:

  • Package-generate отработка аннотаций, связанных с package-info. Компоненты, связанные с ними, обеспечивают загрузку конфигураций и общую подготовку обвязки.
  • Class-generate отработка аннотаций, связанных с классом теста. Здесь выполняются конфигурационные действия, относящиеся к фреймворку, адаптируя его к подготовленной обвязке.
  • Generate отработка аннотаций, связанных с самим методом теста (точкой входа).
  • Test подготовка инстанса и выполнение тестируемого метода.
  • Assert выполнение проверок.

Обрабатываемые аннотации описываются примерно так:

@Target(ElementType.PACKAGE) //аннотация является пакетной@IPhase(value = "package-generate", processingClass = IStabDriver.StabDriverProcessor.class,priority = 1) //указываем фазу и обработчик (обычно обработчик регистрируется в том же классе)public @interface IStabDriver {    Class<? extends WebDriver> value(); //здесь принимаем класс драйвера, который будет загружен вместо настоящего    class StabDriverProcessor implements PhaseProcessor<IStabDriver> { //класс обработчик        @Override        public void process(IStabDriver iStabDriver) {            //подмена драйвера фейковым        }    }}

Особенность Fast-Unit в том, что жизненный цикл можно переопределить для любого класса он описывается аннотацией ITestClass, которая предназначена для указания тестируемого класса и фаз. Список фаз указывается просто как строковый массив, допуская смену состава и последовательность фаз. Методы, обрабатывающие фазы, находятся так же с помощью аннотаций, поэтому возможно создать в своем классе необходимый обработчик и пометить его (плюс к этому доступно переопределение в рамках класса). Большим плюсом стало то, что такое разделение позволило разделить тест на слои: если ошибка в готовом тесте произошла в рамках фазы package-generate или generate, значит повреждена тестовая обвязка. Если class-generate есть проблемы в конфигурационных механизмах фреймворка. Если в рамках test ошибка в тестируемом функционале. Фаза test технически может выдавать ошибки как в обвязке, так и в тестируемом функционале, поэтому возможные ошибки обвязки мы обернули в специальный тип InnerException.

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

Здесь, наверное, уже возник вопрос, а откуда берутся инстансы тестирования. Если конструктор пустой, это очевидно: с помощью Reflection API просто создается инстанс тестируемого класса. Но как в этой конструкции передавать параметры или выполнять настройку инстанса после срабатывания конструктора? Что делать, если объект строится билдером или вообще речь идет о тестировании статики? Для этого разработан механизм провайдеров, которые скрывают за собой сложность конструктора.

Параметризация по умолчанию:

@IProvideInstanceCheckBox generateCheckBox() {    return new CheckBox((MobileElement) ElementProvider.getInstance().provide("//check-box").get());}

Нет параметров нет проблем (мы тестируем класс CheckBox и регистрируем метод, который и будет создавать инстансы для нас). Поскольку здесь переопределен дефолтный провайдер, в самих тестах не потребуется добавлять ничего, они автоматически будут использовать этот метод как источник. Этот пример явно иллюстрирует логику Fast-Unit скрываем сложное и ненужное. С точки зрения теста, совершенно неважно как и откуда возьмется мобильный элемент, оборачиваемый классом CheckBox. Нам важно лишь, что существует некоторый объект CheckBox, удовлетворяющий заданным требованиям.

Автоматическая инъекция аргументов: положим у нас есть вот такой конструктор:

public Mask(String dataFormat, String fieldFormat) {    this.dataFormat = dataFormat;    this.fieldFormat = fieldFormat;}

Тогда тест этого класса с использованием инъекции аргументов будет выглядеть вот так:

Object[] dataMask={"_:2_:2_:4","_:2/_:2/_:4"};@ITestInstance(argSource = "dataMask")@Test@IExpectTestResult(errDesc = "Неверно сконвертировано значение", value = FAST_RESULT,expected = "12/10/2012")public void convert() {    runTest("convert","12102012");}

Именованные провайдеры


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

@IProvideInstance("Дата")Mask createDataMask(){    return new Mask("_:2_:2_:4","_:2/_:2/_:4");} @ITestInstance("Дата")@Test@IExpectTestResult(errDesc = "Неверно сконвертировано значение", value = FAST_RESULT,expected = "12/10/2012")public void convert() {    runTest("convert","12102012");}

IProvideInstance и ITestInstance связанные аннотации, позволяющие указать методу, откуда брать тестируемый инстанс (для статики просто возвращается null, поскольку в конечном итоге этот инстанс используется через Reflection API). Подход с провайдерами дает гораздо больше информации о том, что на самом деле происходит в тесте, заменяя вызов конструктора с какими-то параметрами на текст, описывающий, предварительные условия, так что, если конструктор вдруг поменяется, мы должны будем лишь поправить провайдер, но тест останется неизменным, пока не поменяется реальный функционал. Если же при ревью, вы увидите несколько провайдероов, вы обратите внимание на разницу между ними, а значит и особенности поведения тестируемого метода. Даже совершенно не зная фреймворка, а лишь зная принципы работы Fast-Unit, разработчик сможет прочесть код теста и понять что делает тестируемый метод.

Выводы и итоги


У нашего подхода оказалось немало плюсов:

  • Легкая переносимость тестов.
  • Сокрытие сложности обвязок, возможность их рефакторинга без разрушения тестов.
  • Гарантия обратной совместимости изменения имен методов будут зафиксированы как ошибки.
  • Тесты превратились в достаточно подробную документацию для каждого метода.
  • Качество проверок значительно выросло.
  • Разработка юнит-тестов стала конвейерным процессом, а скорость разработки и ревью значительно выросла.
  • Стабильность разработанных тестов хотя и фреймворк, и сам Fast-Unit активно развиваются, деградации тестов не происходит

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

Текущие проблемы и планы:

  • Рефлексивные вызовы создают некоторую сложность, поскольку не работает прямое перемещение в редакторе и убиваются примитивы. Мы рассматриваем варианты замены рефлексии на интерсепторы, однако при этом потеряются проверки приватных методов (сейчас фаст-юнит игнорирует модификаторы доступа тестируемых методов).
  • Обработчики фаз на данный момент могут обрабатывать только одну фазу.
  • Обработчики фаз определяются однозначно и не могут быть переопределены.
  • На данный момент написано достаточно мало готовых компонентов, то есть сейчас инструмент является в первую очередь именно ядром юнит-тестов.
  • На данный момент Fast-Unit реализован на базе junit4, но в ближайшее время мы собираемся добавить поддержку junit5 и testng
Подробнее..

Из песочницы Apache Kafka и тестирование с Kafka Server

12.11.2020 14:14:33 | Автор: admin

Введение


Существуют различные способы для написания тестов с использованием Apache Kafka. К примеру, можно использовать TestContainers и EmbeddedKafka. Об этом можно почитать, к примеру, вот здесь: Подводные камни тестирования Kafka Streams. Но существует и вариант для написания тестов с использованием KafkaServer.


Что будет тестироваться?


Предположим, необходимо разработать сервис отправки сообщений по различным каналам: email, telegram и т.п.


Пусть имя сервиса будет: SenderService.


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


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


Сервис и тест реализованы с использованием: Java 1.8, Kafka 2.1.0, JUnit 5.5.2, Maven 3.6.1.


Сервис


Сервис будет иметь возможность начать работу и остановить свою работу.


void start()void stop()

При старте необходимо задать, как минимум, следующие параметры:


String bootstrapServersString senderTopicEmailService emailService

bootstrapServers адрес kafka.
senderTopic топик, из которого будут считываться сообщения.
emailService сервис для конечной отправки сообщений по почте.


В реальном сервисе таких конечных сервисов будет столько же сколько и конечных каналов отправки сообщений.


Теперь необходим потребитель, который слушает канал, фильтрует и отправляет сообщения в конечные каналы. Количество таких потребителей можно выбирать. Подход для написания потребителя описан вот здесь: Introducing the Kafka Consumer: Getting Started with the New Apache Kafka 0.9 Consumer Client.


Collection<AutoCloseable> closeables = new ArrayList<>();ExecutorService senderTasksExecutor = Executors.newFixedThreadPool(senderTasksN);ExecutorService tasksExecutorService = Executors.newFixedThreadPool(tasksN);for (int i = 0; i < senderTasksN; i++) {    SenderConsumerLoop senderConsumerLoop =            new SenderConsumerLoop(                    bootstrapServers,                    senderTopic,                    "sender",                    "sender",                    tasksExecutorService,                    emailService            );    closeables.add(senderConsumerLoop);    senderTasksExecutor.submit(senderConsumerLoop);}

В цикле создается экземпляр потребителя, запоминается в коллекции и запускается через сервис запуска задач.


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


Runtime.getRuntime().addShutdownHook(new Thread(() -> {    for (AutoCloseable autoCloseable : closeables) {        try {            autoCloseable.close();        } catch (Exception e) {            e.printStackTrace();        }    }    senderTasksExecutor.shutdown();    tasksExecutorService.shutdown();    stop();    try {        senderTasksExecutor.awaitTermination(5000, TimeUnit.MILLISECONDS);    } catch (InterruptedException e) {        e.printStackTrace();    }}));

При завершении необходимо освободить ресурсы.


Потребитель


Потребитель имеет следующие публичные методы:


void run()void close()

Основной метод: run.


@Overridepublic void run() {    kafkaConsumer = createKafkaConsumerStringString(bootstrapServers, clientId, groupId);    kafkaConsumer.subscribe(Collections.singleton(topic));    while (true) {        calculate(kafkaConsumer.poll(Duration.ofSeconds(1)));    }}

По входным параметрам создается экземпляр kafka-потребителя. kafka-потребитель подписывается на заданный топик. В бесконечном цикле выбираются записи из топика. И отправляются на обработку.


Для иллюстрации json-сообщения будут иметь несколько полей, которые будут задавать и тип сообщения, и данные для отправки.


Пример сообщения:


{  "subject": {    "subject_type": "send"  },  "body": {    "method": "email",    "recipients": "mrbrown@ml.ml;mrblack@ml.ml;mrwhite@ml.ml",    "title": "42",    "message": "73"  }}

subject_type тип сообщения. Для сервиса нужно значение send.
method тип конечного сервиса для отправки. email отправка через почту.
recipients список получателей.
title заголовок для сообщения.
message сообщение.


Обработка всех записей:


void calculate(ConsumerRecords<String, String> records) {    for (ConsumerRecord<String, String> record : records) {        calculate(record);    }}

Обработка одной записи:


void calculate(ConsumerRecord<String, String> record) {            JSONParser jsonParser = new JSONParser();            Object parsedObject = null;            try {                parsedObject = jsonParser.parse(record.value());            } catch (ParseException e) {                e.printStackTrace();            }            if (parsedObject instanceof JSONObject) {                JSONObject jsonObject = (JSONObject) parsedObject;                JSONObject jsonSubject = (JSONObject) jsonObject.get(SUBJECT);                String subjectType = jsonSubject.get(SUBJECT_TYPE).toString();                if (SEND.equals(subjectType)) {                    JSONObject jsonBody = (JSONObject) jsonObject.get(BODY);                    calculate(jsonBody);                }            }        }

Распределение сообщений по типу:


void calculate(JSONObject jsonBody) {    String method = jsonBody.get(METHOD).toString();    if (EMAIL_METHOD.equals(method)) {        String recipients = jsonBody.get(RECIPIENTS).toString();        String title = jsonBody.get(TITLE).toString();        String message = jsonBody.get(MESSAGE).toString();        sendEmail(recipients, title, message);    }}

Отправка в конечную систему:


void sendEmail(String recipients, String title, String message) {    tasksExecutorService.submit(() -> emailService.send(recipients, title, message));}

Отправка сообщений происходит через сервис исполнения задач.


Ожидания завершения отправки не происходит.


Создание kafka-потребителя:


static KafkaConsumer<String, String> createKafkaConsumerStringString(        String bootstrapServers,        String clientId,        String groupId) {    Properties properties = new Properties();    properties.setProperty(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);    properties.setProperty(ConsumerConfig.CLIENT_ID_CONFIG, clientId);    properties.setProperty(ConsumerConfig.GROUP_ID_CONFIG, groupId);    properties.setProperty(            ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG,            "org.apache.kafka.common.serialization.StringDeserializer");    properties.setProperty(            ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,            "org.apache.kafka.common.serialization.StringDeserializer");    properties.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");    return new KafkaConsumer<>(properties);}

Интерфейс для писем:


interface EmailService {    void send(String recipients, String title, String message);}

Тест


Для теста понадобиться следующее.
Адрес kafka-сервера.
Порт для kafka-сервера.
Имя топика.


Сервис для управления kafka-сервером. Будет описан ниже.


public class SenderServiceTest {    @Test    void consumeEmail() throws InterruptedException {        String brokerHost = "127.0.0.1";        int brokerPort = 29092;        String bootstrapServers = brokerHost + ":" + brokerPort;        String senderTopic = "sender_data";        try (KafkaServerService kafkaServerService = new KafkaServerService(brokerHost, brokerPort)) {            kafkaServerService.start();            kafkaServerService.createTopic(senderTopic);        }    }}

Задаются параметры. Создается сервис для управления kafka-сервером. kafka-сервером стартует. Создается необходимый топик.


Создается mock конечного сервиса для отправки сообщений:


SenderService.EmailService emailService = mock(SenderService.EmailService.class);

Создается сам сервис и стартует:


SenderService senderService = new SenderService(bootstrapServers, senderTopic, emailService);senderService.start();

Задаются параметры для сообщения:


String recipients = "mrbrown@ml.ml;mrblack@ml.ml;mrwhite@ml.ml";String title = "42";String message = "73";

Отправляется сообщение в канал:


kafkaServerService.send(senderTopic, key(), createMessage(EMAIL_METHOD, recipients, title, message));

Ожидание:


Thread.sleep(6000);

Проверка, что сообщение дошло до конечного сервиса:


verify(emailService).send(recipients, title, message);

Остановка:


senderService.stop();

Все вместе:


public class SenderServiceTest {    @Test    void consumeEmail() throws InterruptedException {        String brokerHost = "127.0.0.1";        int brokerPort = 29092;        String bootstrapServers = brokerHost + ":" + brokerPort;        String senderTopic = "sender_data";        try (KafkaServerService kafkaServerService = new KafkaServerService(brokerHost, brokerPort)) {            kafkaServerService.start();            kafkaServerService.createTopic(senderTopic);            SenderService.EmailService emailService = mock(SenderService.EmailService.class);            SenderService senderService = new SenderService(bootstrapServers, senderTopic, emailService);            senderService.start();            String recipients = "mrbrown@ml.ml;mrblack@ml.ml;mrwhite@ml.ml";            String title = "42";            String message = "73";            kafkaServerService.send(senderTopic, key(), createMessage(EMAIL_METHOD, recipients, title, message));            Thread.sleep(6000);            verify(emailService).send(recipients, title, message);            senderService.stop();        }    }}

Вспомогательный код:


public class SenderFactory {    public static final String SUBJECT = "subject";    public static final String SUBJECT_TYPE = "subject_type";    public static final String BODY = "body";    public static final String METHOD = "method";    public static final String EMAIL_METHOD = "email";    public static final String RECIPIENTS = "recipients";    public static final String TITLE = "title";    public static final String MESSAGE = "message";    public static final String SEND = "send";    public static String key() {        return UUID.randomUUID().toString();    }    public static String createMessage(String method, String recipients, String title, String message) {        Map<String, Object> map = new HashMap<>();        Map<String, Object> subject = new HashMap<>();        Map<String, Object> body = new HashMap<>();        map.put(SUBJECT, subject);        subject.put(SUBJECT_TYPE, SEND);        map.put(BODY, body);        body.put(METHOD, method);        body.put(RECIPIENTS, recipients);        body.put(TITLE, title);        body.put(MESSAGE, message);        return JSONObject.toJSONString(map);    }}

Сервис для управления kafka-сервером


Основные методы:


void start()void close()void createTopic(String topic)

В методе start происходит создание сервера и вспомогательных объектов.


Создание zookeeper и сохранение его адреса:


zkServer = new EmbeddedZookeeper();String zkConnect = zkHost + ":" + zkServer.port();

Создание клиента zookeeper:


zkClient = new ZkClient(zkConnect, 30000, 30000, ZKStringSerializer$.MODULE$);zkUtils = ZkUtils.apply(zkClient, false);

Задание свойств для сервера:


Properties brokerProps = new Properties();brokerProps.setProperty("zookeeper.connect", zkConnect);brokerProps.setProperty("broker.id", "0");try {    brokerProps.setProperty("log.dirs", Files.createTempDirectory("kafka-").toAbsolutePath().toString());} catch (IOException e) {    throw new RuntimeException(e);}brokerProps.setProperty("listeners", "PLAINTEXT://" + brokerHost + ":" + brokerPort);brokerProps.setProperty("offsets.topic.replication.factor", "1");KafkaConfig config = new KafkaConfig(brokerProps);

Создание сервера:


kafkaServer = TestUtils.createServer(config, new MockTime());

Все вместе:


public void start() {    zkServer = new EmbeddedZookeeper();    String zkConnect = zkHost + ":" + zkServer.port();    zkClient = new ZkClient(zkConnect, 30000, 30000, ZKStringSerializer$.MODULE$);    zkUtils = ZkUtils.apply(zkClient, false);    Properties brokerProps = new Properties();    brokerProps.setProperty("zookeeper.connect", zkConnect);    brokerProps.setProperty("broker.id", "0");    try {        brokerProps.setProperty("log.dirs", Files.createTempDirectory("kafka-").toAbsolutePath().toString());    } catch (IOException e) {        throw new RuntimeException(e);    }    brokerProps.setProperty("listeners", "PLAINTEXT://" + brokerHost + ":" + brokerPort);    brokerProps.setProperty("offsets.topic.replication.factor", "1");    KafkaConfig config = new KafkaConfig(brokerProps);    kafkaServer = TestUtils.createServer(config, new MockTime());}

Остановка сервиса:


@Overridepublic void close() {    kafkaServer.shutdown();    zkClient.close();    zkServer.shutdown();}

Создание топика:


public void createTopic(String topic) {    AdminUtils.createTopic(            zkUtils, topic, 1, 1, new Properties(), RackAwareMode.Disabled$.MODULE$);}

Заключение


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


Для создания и тестирования сервисов с использованием kafka можно обратиться к следующему ресурсу:
kafka-streams-examples


Ссылки и ресурсы


Исходный код


Код для тестирования с kafka-сервером

Подробнее..

Юнит тестирование Spring Bot в Docker и Яндекс облаке

08.02.2021 18:21:29 | Автор: admin

Всем привет.

Меня зовут Евгений Фроликов я разработчик в АльфаСтрахование

В ходе работы над проектом в АльфаСтрахование пишем проект на микро сервисах и сложилось так что один из "микро сервисов" сильно разросся (но до монолита ему ещё далеко :) ). Жили мы так долго и счастливо , пока не стали "переезжать" в облако, тут и начались приключения.

Переезд не чем особенно не запомнился для команды разработки только вопросами от DevOps насчёт портов и т.д. Замечу что все интеграционные тесты мы выпилили для того чтобы отвязаться от зависимости от других команд когда у них что то падает на тестовых стендах. Но стало происходить "магия" в JUnit тестах , а именно стали падать тесты. Падали они фантомное и не предсказуемо , лечилось до поры до времени это retraem pipeline , до тех пор пока эта проблема не стала блокером для выкладок изменений .

тест 1 запусктест 1 запуск

Дальше просто retraem

тест 2 запусктест 2 запуск

И так можно было "крутить рулетку" долго и упорно.

Стали разбираться (спасибо понимающему бизнесу за то что дали на это время) . Так примерно выглядели заголовки наших тестов (которых было ооочень много , так как мы иcпользуем Sonar).

@RunWith(SpringJUnit4ClassRunner.class)@SpringBootTestpublic class ContractStatusServiceTest {    @Autowired    private ContractStatusService contractStatusService;    @MockBean    private RsaInfoComponent rsaInfoComponent;    @MockBean    private ContractRepository contractRepository;

Давайте разберём "магические" анотации

  1. @RunWith(SpringJUnit4ClassRunner.class) -Запуск контейнера Spring для выполнения модульного теста

  2. @SpringBootTest -аннотация говорит Spring Boot пойти и найти основной класс конфигурации (например, с @SpringBootApplication) и использовать его для запуска контекста приложения Spring. SpringBootTest загружает полное приложение

  3. @Autowired - Инжектит Bean;

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

Начались эксперименты такова рода.

@RunWith(SpringRunner.class)@SpringBootTest@RequiredArgsConstructorpublic class  ComponentTestTest {   // @Autowired    private final ComponentTest componentTest;    

То есть попытка проинжектить бин как в приложение , через конструктор

1)@RequiredArgsConstructor - Аннотация Lombok для автоматического создания конструкторов из полей final.

Но.....

java.lang.Exception: Test class should have exactly one public zero-argument constructorat org.junit.runners.BlockJUnit4ClassRunner.validateZeroArgConstructor(BlockJUnit4ClassRunner.java:171)at org.junit.runners.BlockJUnit4ClassRunner.validateConstructor(BlockJUnit4ClassRunner.java:148)at org.junit.runners.BlockJUnit4ClassRunner.collectInitializationErrors(BlockJUnit4ClassRunner.java:127)...

а жаль.

Дальше стали приходить осознавание , а зачем мы поднимаем контекст всего приложения для простых тестов и вспомнили про Mock

@RunWith(MockitoJUnitRunner.class)public class CrossProductServiceTest {    @InjectMocks    private CrossProductService crossProductService;    @Mock    private KaskoService kaskoService;    @Mock    private CrownVirusOfferService crownVirusOfferService;

Давайте разберёмся что тут происходит и всём принципиальная разница

  1. @RunWith(MockitoJUnitRunner.class) - Заполняет заглушками наш Bean , а не поднимается контекст (подробнее можно почитать в доках )

  2. @Mock - сама заглушка

  3. @InjectMocks - создаёт Bean и передаёт в конструктор заглушки

И всё "звалось".

Плюсы :

  1. У нас в разы ускорились тесты при деплоях (так как контекст не поднимается)

  2. Мы перестали ловить и бояться не проинжектеных бинов

Минусы:

  1. Пришлось переписывать все тесты

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru