Сравнение библиотек утверждений

Ссылка на оригинал - https://blog.frankel.ch/comparison-assertion-libraries/, автор публикации - Nicolas Fränkel

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

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

Текущее состояние библиотек утверждений

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

  1. FEST Assert. Он был частью большого набора FEST, который включал довольно популярную библиотеку тестирования Swing. В настоящее время FEST не находится в стадии активной разработки.
  2. Hamcrest. Hamcrest - это библиотека утверждений, доступная для всех основных языков (Java, Python, Ruby и т. Д.). Несколько лет назад он стал справочной библиотекой для утверждений.
  Этот список не был бы полным даже без ссылки на Google Truth. Однако, я чувствую, что это никогда не получало никакой тяги, независимо от брендинга Google.

И все же 2 года назад команда проекта, над которой я работал, решила использовать AssertJ для утверждений. Я понятия не имею, почему, и я могу ошибаться, но кажется, что AssertJ довольно популярен в наши дни. Проверка соответствующих репозиториев на Github также показывает, что коммиты Hamcrest больше, но более редки по сравнению с AssertJ. Наконец, AssertJ предоставляет конкретные утверждения для Guava, Joda Time, Neo4J, Swing (!) И баз данных.

В этом посте я бы хотел сравнить 3 библиотеки:

  1. AssertJ - это будет использоваться в качестве ссылки в этом посте
  2. Strikt
  3. Atrium
  4.  

Образец модели

Далее я буду использовать модель, бесстыдно взятую из документации AssertJ:

data class TolkienCharacter(val name: String,
                            val race: Race,
                            val age: Int? = null)

enum class Race(val label: String) {
    HOBBIT("Hobbit"), MAN("Man"), ELF("Elf"), DWARF("Dwarf"), MAIA("Maia")
}

val frodo = TolkienCharacter("Frodo", HOBBIT, 33)
val sam = TolkienCharacter("Gimli", DWARF)
val sauron = TolkienCharacter("Sauron", MAIA)
val boromir = TolkienCharacter("Boromir", MAN, 37)
val aragorn = TolkienCharacter("Aragorn", MAN)
val legolas = TolkienCharacter("Legolas", ELF, 1000)
val fellowshipOfTheRing = listOf(
        boromir,
        TolkienCharacter("Gandalf", MAN),
        aragorn,
        TolkienCharacter("Sam", HOBBIT, 38),
        TolkienCharacter("Pippin", HOBBIT),
        TolkienCharacter("Merry", HOBBIT),
        frodo,
        sam,
        legolas)

Особенности AssertJ

Чтобы начать использовать AssertJ, просто добавьте следующую зависимость в POM:

<dependency>
    <groupId>org.assertj</groupId>
    <artifactId>assertj-core</artifactId>
    <version>3.11.1</version>
    <scope>test</scope>
</dependency>

На самом базовом уровне AssertJ позволяет проверить на равенство и одинаковость:

@Test
fun `assert that frodo's name is equal to Frodo`() {
  assertThat(frodo.name).isEqualTo("Frodo")
}

@Test
fun `assert that frodo is not sauron`() {
    assertThat(frodo).isNotSameAs(sauron)
}
  Kotlin позволяет имени функции содержать пробелы при условии, что имя отделяется обратными чертами. Это очень полезно для имен утверждений.

AssertJ также предлагает другое утверждение для строк:

@Test
fun `assert that frodo's name starts with Fro and ends with do`() {
    assertThat(frodo.name)
            .startsWith("Fro")
            .endsWith("do")
            .isEqualToIgnoringCase("frodo")
}

Наконец, AssertJ действительно сияет при утверждении коллекций:

@Test
fun `assert that fellowship of the ring members' names contains Boromir, Gandalf, Frodo and Legolas and does not contain Sauron and Elrond`() {
  assertThat(fellowshipOfTheRing).extracting<String>(TolkienCharacter::name) 
    .doesNotContain("Sauron", "Elrond")
}

@Test
fun `assert that fellowship of the ring members' name containing 'o' are only aragorn, frodo, legolas and boromir`() {
  assertThat(fellowshipOfTheRing).filteredOn { it.name.contains("o") }       
    .containsOnly(aragorn, frodo, legolas, boromir)
}

@Test
fun `assert that fellowship of the ring members' name containing 'o' are of race HOBBIT, ELF and MAN`() {
  assertThat(fellowshipOfTheRing).filteredOn { it.name.contains("o") }       
    .containsOnly(aragorn, frodo, legolas, boromir)
    .extracting<String> { it.race.label }
    .contains("Hobbit", "Elf", "Man")
}
  extracting() аналогично, map () но в контексте утверждений
  Аналогично, filteredOn() похож на filter()
  filteredOn() и extracting() могут быть объединены для уточнения утверждений в «конвейере утверждений»

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

org.opentest4j.AssertionFailedError:
Expecting:
 <33>
to be equal to:
 <44>
but was not.

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

@Test
fun `assert that frodo's age is 33`() {
    assertThat(frodo.age).`as`("%s's age", frodo.name).isEqualTo(44)
}
org.opentest4j.AssertionFailedError: [Frodo's age]
Expecting:
 <33>
to be equal to:
 <44>
but was not.

Особенности Стрикта

Strikt - это библиотека утверждений, написанная на Kotlin. Его документация довольно обширна и читабельна.

Strikt - это библиотека утверждений для Kotlin, предназначенная для использования с тестовыми программами, такими как JUnit или Spek.
  Ничто не мешает использовать его с TestNG.

Чтобы начать использовать Strikt, добавьте этот фрагмент зависимости в POM:

<dependency>
    <groupId>io.strikt</groupId>
    <artifactId>strikt-core</artifactId>
    <version>0.16.0</version>
    <scope>test</scope>
</dependency>

Strikt предлагает AssertJ эквивалентные функции в отношении простого использования. Его API отображает почти один к одному:

@Test
fun `assert that frodo's name is equal to Frodo`() {
    expectThat(frodo.name).isEqualTo("Frodo")
}

@Test
fun `assert that frodo is not sauron`() {
    expectThat(frodo).isNotSameInstanceAs(sauron)
}

@Test
fun `assert that frodo starts with Fro and ends with do`() {
    expectThat(frodo.name)
            .startsWith("Fro")
            .endsWith("do")
            .isEqualToIgnoringCase("frodo")
}

Стрикт также предлагает утверждения по коллекциям:

@Test
fun `assert that fellowship of the ring has size 9, contains frodo and sam, and does not contain sauron`() {
  expectThat(fellowshipOfTheRing)
    .hasSize(9)
    .contains(frodo, sam)
    .doesNotContain(sauron)
}

Однако, нет функции, соответствующей extracting()nor filteredOn(): Следовательно, по умолчанию следует вернуться к использованию map()и filter():

@Test
fun `assert that fellowship of the ring members' names contains Boromir, Gandalf, Frodo and Legolas and does not contain Sauron and Elrond`() {
  expectThat(fellowshipOfTheRing).map { it.name }
    .contains("Boromir", "Gandalf", "Frodo", "Legolas")
    .doesNotContain("Sauron", "Elrond")
}

@Test
fun `assert that fellowship of the ring members' name containing 'o' are only aragorn, frodo, legolas and boromir`() {
  expectThat(fellowshipOfTheRing.filter { it.name.contains("o") })
    .containsExactlyInAnyOrder(aragorn, frodo, legolas, boromir)
}

Использование стандартного API не позволяет связывать утверждения, как это возможно в AssertJ. Чтобы компенсировать это, можно сгруппировать утверждения вместе через expect()функцию, которая принимает лямбду:

@Test
fun `assert that fellowship of the ring members' name containing 'o' are of race HOBBIT, ELF and MAN`() {
  expect {
    that(fellowshipOfTheRing.filter { it.name.contains("o") })
      .containsExactlyInAnyOrder(aragorn, frodo, legolas, boromir)
    that(fellowshipOfTheRing).map { it.race.label }
      .contains("Hobbit", "Elf", "an")
  }
}

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

org.opentest4j.AssertionFailedError:
▼ Expect that 33:
  ✗ is equal to 44 : found 33

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

strikt.internal.opentest4j.CompoundAssertionFailure:
▼ Expect that […]:
  ✓ contains exactly the elements […] in any order
    ✓ contains TolkienCharacter(name=Aragorn, race=MAN,…
    ✓ contains TolkienCharacter(name=Frodo, race=HOBBIT…
    ✓ contains TolkienCharacter(name=Legolas, race=ELF,…
    ✓ contains TolkienCharacter(name=Boromir, race=MAN,…
    ✓ contains no further elements
▼ Expect that […]:
  ▼ ["Man", "Man", "Man", "Hobbit"…]:
    ✗ contains the elements ["Hobbit", "Elf", "an"]
      ✓ contains "Hobbit"
      ✓ contains "Elf"
      ✗ contains "an"

Сообщения также могут быть сделаны более наглядными:

@Test
fun `assert that frodo's age is 33`() {
    expectThat(frodo.age).describedAs("${frodo.name}'s age").isEqualTo(44)
}
org.opentest4j.AssertionFailedError:
▼ Expect that Frodo's age:
  ✗ is equal to 44 : found 33
  Нет доступной сигнатуры метода для передачи дополнительного объекта, в отличие от AssertJ as(). Однако в этом нет необходимости из-за возможности интерполяции строк в Kotlin.

Атриум

Atrium - еще одна библиотека утверждений, написанная на Kotlin.

Atrium предназначен для поддержки различных API, разных стилей отчетности и интернационализации (i18n). Ядро Atrium, а также конструкторы, создающие сложные утверждения, предназначены для расширения и, таким образом, позволяют легко расширять или заменять компоненты.

Это очень мощный, но и довольно сложный по сравнению с AssertJ и Strikt.

Первый шаг - выбрать JAR (ы), от которых зависит. Атриум доступен в нескольких вариантах:

Infix-ориентированной

Infix позволяет вызывать свободный API без точек:

assert(x).toBe(2)
assert(x) toBe 2
глагол

Глаголом утверждения по умолчанию является assert(). Два других глагола доступны из коробки: assertThat()и check(). Также возможно создать свой собственный глагол.

локализованный

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

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

<dependency>
    <groupId>ch.tutteli.atrium</groupId>
    <artifactId>atrium-cc-en_GB-robstoll</artifactId>
    <version>0.7.0</version>
    <scope>test</scope>
</dependency>

Основные утверждения очень похожи на утверждения AssertJ и Strikt'а:

@Test
fun `assert that frodo's name is equal to Frodo`() {
  assert(frodo.name).toBe("Frodo")
}

@Test
fun `assert that frodo is not sauron`() {
  assert(frodo).isNotSameAs(sauron)
}

Тем не менее, API Atrium допускает альтернативный, полностью типобезопасный способ написания:

@Test
fun `assert that frodo's name is equal to Frodo 2`() {
  assert(frodo) {
    property(subject::name).toBe("Frodo")
  }
}

Он может адаптироваться в зависимости от собственного вкуса. Вот 4 различных способа написания одного и того же утверждения на String:

@Test
fun `assert that frodo starts with Fro and ends with do`() {
  assert(frodo.name)
    .startsWith("Fro")
    .endsWith("do")
    .isSameAs("Frodo")
}

@Test
fun `assert that frodo starts with Fro and ends with do 2`() {
  assert(frodo.name) {
    startsWith("Fro")
    endsWith("do")
    isSameAs("Frodo")
  }
}

@Test
fun `assert that frodo starts with Fro and ends with do 3`() {
  assert(frodo) {
    property(subject::name)
      .startsWith("Fro")
      .endsWith("do")
      .isSameAs("Frodo")
  }
}

@Test
fun `assert that frodo starts with Fro and ends with do 4`() {
  assert(frodo) {
    property(subject::name) {
      startsWith("Fro")
      endsWith("do")
      isSameAs("Frodo")
    }
  }
}

Как и AssertJ и Strikt, Atrium предлагает API для выполнения утверждений в коллекциях:

@Test
fun `assert that fellowship of the ring has size 9, contains frodo and sam, and does not contain sauron`() {
  assert(fellowshipOfTheRing)
    .hasSize(9)
    .contains(frodo, sam)
    .containsNot(sauron)
}

@Test
fun `assert that fellowship of the ring members' names contains Boromir, Gandalf, Frodo and Legolas and does not contain Sauron and Elrond`() {
  assert(fellowshipOfTheRing.map { it.name })                             
    .containsNot("Sauron", "Elrond")                                       
}

@Test
fun `assert that fellowship of the ring members' name containing 'o' are only aragorn, frodo, legolas and boromir`() {
  assert(fellowshipOfTheRing.filter { it.name.contains("o") })            
    .contains.inAnyOrder.only.values(aragorn, frodo, legolas, boromir)     
}
  Как и у Strikt, у Atrium нет специального API для карты и фильтра. Надо полагаться на API Kotlin.
  Классическая содержит / не содержит утверждений доступны.
  Сокращенное утверждение
  Полноценное настраиваемое утверждение

Я не нашел способа уточнить утверждения в конвейере. Единственный вариант - вызвать разные утверждения:

@Test
fun `assert that fellowship of the ring members' name containing 'o' are of race HOBBIT, ELF and MAN`() {
  val fellowshipOfTheRingMembersWhichNameContainsO = fellowshipOfTheRing.filter { it.name.contains("o") }
  assert(fellowshipOfTheRingMembersWhichNameContainsO)
    .contains.inAnyOrder.only.values(aragorn, frodo, legolas, boromir)
  assert(fellowshipOfTheRingMembersWhichNameContainsO.map { it.race.label }.distinct())
    .containsStrictly("Hobbit", "Elf", "Man")
}

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

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

Заключение

AssertJ - это довольно хорошая библиотека утверждений Java. Он имеет некоторые незначительные ограничения, некоторые из которых исходят от Java, некоторые от самого API.

Strikt очень похож на AssertJ, но исправляет эти ограничения. Если вы используете Kotlin, его можно использовать в качестве замены.

Atrium также написан на Kotlin, но предлагает гораздо больше возможностей за счет довольно большой сложности.