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

    В одном из старых проектов в кучу были навалены ассерты из 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: 1
      but was : 2
      at 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 (https://goo.gl/aH3UyP)]].(:0)
      at [[Testing framework: 27 frames collapsed (https://goo.gl/aH3UyP)]].(:0)
      at java.base/java.util.ArrayList.forEach(ArrayList.java:1540)
      at [[Testing framework: 9 frames collapsed (https://goo.gl/aH3UyP)]].(:0)
      at java.base/java.util.ArrayList.forEach(ArrayList.java:1540)
      at [[Testing framework: 9 frames collapsed (https://goo.gl/aH3UyP)]].(:0)
      at java.base/java.util.ArrayList.forEach(ArrayList.java:1540)
      at [[Testing framework: 17 frames collapsed (https://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 3
      but 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. Надеюсь, что и вам эта статья пригодилась и поможет вам выбрать библиотеку для ассертов, подходящую под ваши нужды.

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

    А какая библиотека больше всего нравится вам?

    • 28,6%JUnit6
    • 4,8%kotlin.test1
    • 52,4%AssertJ11
    • 14,3%Kotest3
    • 0,0%Truth0
    • 19,0%Hamсrest4
    • 0,0%Strikt0
    • 9,5%Kluent2
    • 0,0%Atrium0
    • 0,0%Expekt0
    • 0,0%AssertK0
    • 0,0%HamKrest0
    • 4,8%пофиг, лишь бы ассертило1
    • 0,0%сложные ассерты не нужны0
    • 4,8%другая (напишу в комментах)1

    Какой библиотекой пользуетесь?

    • 42,3%JUnit11
    • 7,7%kotlin.test2
    • 46,2%AssertJ12
    • 3,8%Kotest1
    • 0,0%Truth0
    • 19,2%Hamсrest5
    • 0,0%Strikt0
    • 3,8%Kluent1
    • 0,0%Atrium0
    • 0,0%Expekt0
    • 0,0%AssertK0
    • 0,0%HamKrest0
    • 0,0%FestAssert (ну а вдруг)0
    • 0,0%другой (напишу в комментах)0
    • 0,0%никакой, и так норм0
    • 15,4%не пишу тесты4
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 7

      0
      AssertJ… Интеграция: нет <Click to see difference>

      Хм, у меня есть:

        0

        А какая у вас версия IntelliJ?

          0

          Вот как выглядит у меня что в Community 2020.1.2, что в 2019.3.3


          Скриншот

          image

            +1

            Проверял и в 2020.2 Beta, и 2020.1.2.


            Но дело, похоже, в другом: у вас выбрасывается исключение org.opentest4j.AssertionFailedError. Если я добавлю зависимость testImplementation "org.opentest4j:opentest4j:1.2.0", то у меня становится так же, без "Click to see difference". Но без этой зависимости тест выбрасывает org.junit.ComparisonFailure, и идея это парсит нормально. Если что, зависимости выглядят так:


            dependencies {
                implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
                testCompile group: 'junit', name: 'junit', version: '4.12'
                testImplementation "org.assertj:assertj-core:3.16.1"
            //    testImplementation "org.opentest4j:opentest4j:1.2.0"  // с этим не будет "Click to see difference"
            }
              +1

              Занятно, на это даже есть тикет у AssertJ. Вечером посмотрю, т.к. opentest4j в проекте идет от junit-jupiter-engine и не очень понятно, как правильно от этого избавиться.

                +1

                ookami_kb
                Судя по всему, вы запускаете с использованием JUnit4. В нем действительно оборачивается простой ассерт в org.junit.ComparisonFailure. Однако ассерт чуть посложнее уже не будет оборачиваться:


                Скриншот

                image


                С JUnit5 ситуация чуть повеселее. Есть баг в IntelliJ, суть которого в том, что интеграция не всегда работает, если запускать тесты через gradle (как это раньше делал я). И еще один связанный баг зарелизят в следующей версии.


                В итоге если запускать тесты не через gradle, а через Idea (настраивается это через Settings → Build, Execution, Deployment → Gradle → Run tests using), то и в JUnit5 будет показываться сравнение, но только для простого isEqualTo:


                Скриншот

                image


                Спасибо за ваше замечание, узнал много интересного:)

        0
        expectThat(haystack).not().contains(needle)

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

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

        Самое читаемое