Рубрики
Без рубрики

Эволюция утверждений в тестах Java

Обзор подходов к тестированию утверждений. С тегами java, kotlin, тестирование, утверждения.

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

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

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

public class Person {

    private String name;
    private Date birthDate;
    private final Set hobbies = new HashSet<>();

    public Person(){}

    public String getName() { 
        return this.name; 
    }

    public void setName(String name){ 
        this.name = name; 
    }

    public Date getBirthDate() { 
        if(this.birthDate == null) { 
            return null; 
        }
        return new Date(this.birthDate.getTime()); 
    }

    public void setBirthDate(Date date) { 
        this.date = new Date(date.getTime()); 
    }

    public Set getHobbies() { 
        return Collections.unmodifiableSet(this.hobbies); 
    }

    public void setHobbies(Set hobbies) {
        this.hobbies.clear();
        if(hobbies != null) { 
            this.hobbies.addAll(hobbies); 
        }
    }

}

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

Самый простой способ сформулировать утверждение в Java (без каких-либо фреймворков) заключается в следующем:

if(someCondition){
    throw new AssertionError("Hey I didn't expect that!");
}

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

  • Сделайте его более кратким . Сократите объем кода, который необходимо написать.
  • Сделайте его более беглым . Предоставьте какой-то синтаксис конструктора.
  • Автоматически генерируйте сообщение таким образом, чтобы оно никогда не выходило из синхронизации с тестом.

JUnit поставляется с классом, который просто называется Assert . Этот класс состоит из серии статических методов, которые должны помочь пользователю в написании кратких утверждений. Большинство методов имеют следующую форму:

public static void assertXYZ(message, expectedValue, actualValue) { /* ... */ }

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

import org.junit.*

public class PersonTest {

    @Test
    public void testWithAssert(){
        Person p = new Person();
        Assert.assertNull("A new Person should not have an initial name.", p.getName());
        p.setName("John Doe");
        Assert.assertEquals("John Doe", p.getName());
    }

}

Кроме того, обычно используется import static org.junit. Утверждать. * для статического импорта методов утверждения. Таким образом, Assert. перед утверждением может быть опущен.

Это уже большой шаг вперед по сравнению с первоначальной конструкцией if : она помещается в одну строку. Однако при таком подходе существует несколько проблем:

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

Через несколько лет после выпуска JUnit была выпущена изящная маленькая библиотека с (очень странным) названием Hamcrest . Hamcrest был построен вокруг идеи о том, что утверждение по сути является критерием соответствия , и как таковое оно должно быть представлено объектом Matcher . Это обеспечивает большую вариативность утверждений с улучшенными сообщениями об ошибках и более плавным синтаксисом:

@Test
public void hamcrestTest(){
    Person p = new Person();

    assertThat(p.getHobbies(), is(empty());
    assertThat(p.getName(), is(nullValue());

    p.setName("John Doe");
    p.setBirthDate(new Date());
    Set newHobbies = new HashSet<>();
    newHobbies.add("coding");
    newHobbies.add("blogging");
    newHobbies.add("reading");
    p.setHobbies(newHobbies);

    assertThat(p.getName(), is("John Doe");
    assertThat(p.getHobbies(), not(hasItem("programming"));
    assertThat(p.getHobbies(), containsInAnyOrder("coding", "blogging", "programming");
    assertThat(p.getHobbies().size(), is(3));
    assertThat(p.getBirthDate().getTime(), is(greaterThan(0L));

}

Как вы можете видеть, метод assertThat(value, matcher) является точкой входа. В отличие от assertEquals , совершенно ясно, какое значение является ожидаемым , а какое – фактическим значением, так что это большой плюс сразу. Недостатком является то, что из-за того, что утверждайте, что(...) имеет так много различных перегрузок, что вы не можете использовать assertThat(p.getName(), is(null)) , потому что null делает неоднозначным, какое переопределение использовать. Вместо этого вам нужно использовать null Value() , который по сути является просто сопоставителем, который проверяет равенство с null .

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

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

// alright, let's test the name...
p.getName()|

// ... oh, I forgot the assertThat...
assertThat(|p.getName()

// ... moves carret all the way back again...
assertThat(p.getName(), |

// ok IDE, I need the "is" method, do your magic!
assertThat(p.getName(), is|

// (200 completion proposals show up)
// *sigh*
assertThat(p.getName(), is("John Doe"));|

// finally!

Понимаете, что я имею в виду? К счастью, некоторые люди сталкивались с теми же проблемами.

Truth – это библиотека, похожая на Hamcrest, за исключением того, что она предлагает fluent builder API:

@Test
public void testTruth(){
    Person p = new Person();

    assertThat(p.getName()).isNull();
    Set newHobbies = new HashSet<>();
    newHobbies.add("coding");
    newHobbies.add("blogging");
    newHobbies.add("reading");
    p.setHobbies(newHobbies);

    assertThat(p).containsExactly("coding", "blogging", "reading");
}

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

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

Теперь мы покидаем безопасную гавань Явы и отправляемся в дикую природу. Как оказалось, Kotlin хорошо подходит для создания тестового мини-фреймворка, который состоит всего из двух функций (и библиотеки Hamcrest).:

infix fun  T.shouldBe(other: T): Unit {
    assertThat(this, is(other));
}

infix fun  T.shouldBe(matcher: Matcher): Unit {
    assertThat(this, matcher);
}

“Как это помогает?” и “что это, черт возьми, такое?” – спросите вы. Итак, мы определяем две функции расширения , которые находятся на Объекте (или, в Kotlin: Любой ). Что означает: теперь мы можем вызвать x.должно Быть(...) на чем угодно, независимо от того, что такое x . Кроме того, это функция infix , что означает, что мы можем отбросить точку, открывающую и закрывающую скобки.

Проверить это:

@Test
fun testKotlin(){
    Person p = Person()

    p.name shouldBe null
    p.name = "John Doe"
    p.name shouldBe "John Doe"

    p.hobbies = setOf("coding", "blogging", "reading")
    p.hobbies.size shouldBe 3

    p.birthDate = Date()
    p.birthDate!!.time shouldBe greaterThan(0L)

}

Теперь это именно та читаемость, которую я ищу!

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

  • Спок (Заводное тестирование)
  • Огурец (Происхождение синтаксиса теста Gherkin)

Надеюсь, вам понравилась эта небольшая экскурсия по библиотекам. Не стесняйтесь делиться своим опытом и давать рекомендации по библиотекам и/или стилям кодирования, которые я пропустил!

Оригинал: “https://dev.to/martinhaeusler/the-evolution-of-assertions-in-java-tests-2633”