Як перевірити JUnit, що два списки <E> містять однакові елементи в однаковому порядку?


77

Контекст

Я пишу простий тест JUnit дляMyObject класу.

A MyObject може бути створений із статичного фабричного методу, який приймає varargs з String .

MyObject.ofComponents("Uno", "Dos", "Tres");

У будь-який час протягом існування MyObjectклієнти можуть перевірити параметри, за якими він був створений, у формі List <E> , за допомогою.getComponents()методу.

myObject.ofComponents(); // -> List<String>: { "Uno", "Dos", "Tres" }

Іншими словами, MyObjectі запам'ятовує, і виставляє список параметрів, які його створили . Детальніше про цей контракт:

  • Порядок getComponentsбуде таким же, як і вибраний для створення об’єкта
  • Повторювані наступні компоненти рядка дозволяються та зберігаються в порядку
  • Поведінка на nullне визначена (інший код гарантує відсутність nullпотрапляння на завод)
  • Немає способів змінити список компонентів після створення об'єкта

Я пишу простий тест, який створює MyObjectзі списку рядків і перевіряє, чи може він повернути той самий список через .getComponents(). Я роблю це негайно, але це повинно відбуватися на відстані в реалістичному шляху коду .

Код

Ось моя спроба:


List<String> argumentComponents = Lists.newArrayList("One", "Two", "Three");
List<String> returnedComponents =
    MyObject.ofComponents(
        argumentComponents.toArray(new String[argumentComponents.size()]))
        .getComponents();
assertTrue(Iterables.elementsEqual(argumentComponents, returnedComponents));

Питання

  • Чи є Google Guava Iterables.elementsEqual() найкращим способом порівняти ці два списки за умови, що у мене є бібліотека на шляху побудови? це те, про що я мучився; чи слід використовувати цей допоміжний метод, який переходить Iterable <E> .. перевірити розмір, а потім повторити запуск .equals().. або будь-який інший із методів, запропонованих в Інтернеті? який канонічний спосіб порівняння списків для модульних тестів?

Необов’язкові ідеї, які я хотів би отримати

  • Чи обґрунтовано розроблений тест методу? Я не фахівець у JUnit !
  • Чи .toArray()найкращий спосіб перетворити List <E> у вараги E?

15
+1 за добре структуроване запитання.
maba

1
Hamcrest має IsIterableContainingInOrderякий призначений саме для тестування, на відміну від Iterables. Використання Hamcrest дасть хороші повідомлення на випадок невдачі.
Джон Б

fest-assert забезпечує елегантний вільний api ...
gontard

@JohnB: Hamcrest чудовий та ідеальний для тестування, саме про це я і просив (thx!), Але в цьому конкретному випадку IsIterableContainingInOrderнедостатньо суворо, правда? M={A,B,C}містить N={A,B}у порядку, але M!=N.
Robottinosino

@Robottinosino За повідомленням javadocCreates a matcher for Iterables that matches when a single pass over the examined Iterable yields a series of items, each logically equal to the corresponding item in the specified items. For a positive match, the examined iterable must be of the same length as the number of specified items
John B

Відповіді:


57

Я вважаю за краще використовувати Hamcrest, оскільки він дає набагато кращий результат у випадку відмови

Assert.assertThat(listUnderTest, 
       IsIterableContainingInOrder.contains(expectedList.toArray()));

Замість звітування

expected true, got false

він повідомить

expected List containing "1, 2, 3, ..." got list containing "4, 6, 2, ..."

IsIterableContainingInOrder.contain

Гамкрест

Відповідно до Javadoc:

Створює збіг для Iterables, який збігається, коли один прохід над обстежуваним Iterable дає ряд елементів, кожен логічно дорівнює відповідному елементу у зазначених елементах. Для позитивного збігу обстежуваний ітератор повинен мати однакову довжину із кількістю зазначених елементів

Отже, listUnderTestобов’язково має бути однакова кількість елементів, і кожен елемент повинен відповідати очікуваним значенням по порядку.


1
IsIterableContainingInOrder недостатньо жорсткий для перевірки рівності?
Robottinosino

що ти маєш на увазі / про що ти питаєш?
Джон Б

Результат для Hamcrest такий самий, як і для звичайних тверджень jUnit. assertEquals (очікуваний, фактичний); буде виводити: java.lang.AssertionError: очікується: <[1, 2]>, але було: <[3, 4]> Я не думаю, що Hamcrest має якісь переваги в засобах виводу
Генрік

@henrik - Насправді результат від Hamcrest є Expected: iterable containing [<1>, <3>, <5>, <7>] but: item 3: was <6>. У довгому списку елементів, toStringвихід яких може бути досить довгим, знання, який елемент є неправильним, може бути дуже корисним.
Джон Б,

1
IsIterableContainingInOrder.contain()оманлива назва. Це не відображає той факт, що воно додатково перевіряє наявність actualоднакової кількості елементів (перевірка рівності розміру). hasSameElementsAs()було б доречнішою назвою
Lu55

75

Чому б просто не використовувати List#equals?

assertEquals(argumentComponents, imapPathComponents);

ДоговірList#equals :

два списки визначено рівними, якщо вони містять однакові елементи в однаковому порядку.


Який саме матч від Hamcrest? Я міг би використовувати точні повідомлення про помилки твердження, що надаються безкоштовно ...
Robottinosino

1
Як за те, що не використовував зовнішніх бібліотек
Spektakulatius

Цей код IMHO виглядає набагато зрозумілішим, ніж код Hamcrest IsIterableContainingInOrder.contain()(назва якого не повністю відображає його поведінку). Отже, якщо у вас є колекції, крім List <>, то краще перетворити їх у List і використовувати цей код, а не масив, який вимагає Hamcrest.
Lu55, 02

10

Метод equals () у вашій реалізації List повинен робити поелементне порівняння, отже

assertEquals(argumentComponents, returnedComponents);

набагато простіше.


10

org.junit.Assert.assertEquals()і org.junit.Assert.assertArrayEquals()виконувати роботу.

Щоб уникнути наступних питань: Якщо ви хочете ігнорувати замовлення, поставте всі елементи для встановлення, а потім порівняйте: Assert.assertEquals(new HashSet<String>(one), new HashSet<String>(two))

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

Ще одна порада. Фокус Assert.assertEquals(new HashSet<String>(one), new HashSet<String>(two))працює добре, поки не вдається порівняння. У цьому випадку він показує повідомлення про помилку з рядковими поданнями ваших наборів, що може заплутати, оскільки порядок у наборі майже не передбачуваний (принаймні для складних об'єктів). Отже, фокус, який я знайшов, полягає в тому, щоб замістити колекцію впорядкованим набором HashSet. Ви можете використовувати TreeSetспеціальний компаратор.


7
Використання HashSet не збереже замовлення та дублікати .
gontard

5

Для чудової читабельності коду Fest Assertions має гарну підтримку для затвердження списків

Тож у цьому випадку щось на зразок:

Assertions.assertThat(returnedComponents).containsExactly("One", "Two", "Three");

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

Assertions.assertThat(returnedComponents).containsExactly(argumentComponents.toArray());

3

assertTrue () / assertFalse (): використовувати лише для затвердження булевого результату, що повертається

assertTrue (Iterables.elementsEqual (argumentComponents, returnComponents));

Ви хочете використовувати Assert.assertTrue()або Assert.assertFalse()як тестований метод повертає booleanзначення.
Оскільки метод повертає конкретну річ, таку як Listяка повинна містити деякі очікувані елементи, твердження assertTrue()таким чином: Assert.assertTrue(myActualList.containsAll(myExpectedList) є анти шаблоном.
Це полегшує написання твердження, але оскільки тест не вдається, це також ускладнює налагодження, оскільки тест-драйвер скаже вам лише щось на зразок:

очікуваний, trueале фактичний єfalse

Assert.assertEquals(Object, Object)в JUnit4 або Assertions.assertIterableEquals(Iterable, Iterable)в JUnit 5: використовувати лише як обидва, equals()і toString()перевизначені для класів (і глибоко) порівняних об'єктів

Це має значення, оскільки тест на рівність у твердженні покладається, equals()а повідомлення про помилку тесту toString()- на порівнювані об’єкти.
Оскільки Stringзамінює обидва equals()і toString(), цілком справедливо стверджувати List<String>с assertEquals(Object,Object). А щодо цього: вам доведеться перевизначити equals()в класі, оскільки це має сенс з точки зору рівності об’єктів, а не лише для полегшення тверджень у тесті з JUnit.
Для полегшення тверджень у вас є інші способи (це ви можете побачити в наступних пунктах відповіді).

Чи є Гуава способом виконати / побудувати твердження щодо модульних тестів?

Чи є Google Guava Iterables.elementsEqual () найкращим способом порівняти ці два списки за умови, що у мене є бібліотека на шляху до збірки?

Ні, це не так. Гуава - це не бібліотека для написання тверджень модульного тесту.
Вам це не потрібно для написання більшості (все, що я думаю) з модульних тестів.

Який канонічний спосіб порівняння списків для модульних тестів?

Як хорошу практику я віддаю перевагу бібліотекам тверджень / збігів.

Я не можу заохотити JUnit виконувати конкретні твердження, оскільки це забезпечує насправді занадто мало і обмежених можливостей: він виконує лише твердження з глибокими рівними.
Іноді ви хочете дозволити будь-який порядок в елементах, іноді ви хочете дозволити, щоб будь-які елементи очікуваного збігалися з фактичними, і так для ...

Отже, правильним способом є використання бібліотеки тверджень / збігів для модульного тестування, таких як Hamcrest або AssertJ.
Фактична відповідь забезпечує рішення Гамкреста. Ось рішення AssertJ .

org.assertj.core.api.ListAssert.containsExactly() це те, що вам потрібно: він перевіряє, що фактична група містить точно задані значення і нічого іншого, в порядку, як зазначено:

Перевіряє, що фактична група містить точно вказані значення і нічого іншого, по порядку.

Ваш тест може виглядати так:

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;

@Test
void ofComponent_AssertJ() throws Exception {
   MyObject myObject = MyObject.ofComponents("One", "Two", "Three");
   Assertions.assertThat(myObject.getComponents())
             .containsExactly("One", "Two", "Three");
}

Хорошим моментом у AssertJ є те, що оголошувати Listочікуване без потреби: це робить твердження випрямленішим, а код більш читабельним:

Assertions.assertThat(myObject.getComponents())
         .containsExactly("One", "Two", "Three");

І якщо тест не вдається:

// Fail : Three was not expected 
Assertions.assertThat(myObject.getComponents())
          .containsExactly("One", "Two");

ви отримуєте дуже чітке повідомлення, таке як:

java.lang.AssertionError:

Очікуючи:

<["Один", "Два", "Три"]>

містити точно (і в тому самому порядку):

<["Один", "Два"]>

але деякі елементи не очікувались:

<["Три"]>

Бібліотеки тверджень / збігів є обов’язковими, оскільки вони дійсно будуть надалі

Припустимо , що MyObjectне зберігає Strings , але FooS примірників таких як:

public class MyFooObject {

    private List<Foo> values;
    @SafeVarargs
    public static MyFooObject ofComponents(Foo... values) {
        // ...
    }

    public List<Foo> getComponents(){
        return new ArrayList<>(values);
    }
}

Це дуже поширена потреба. З AssertJ твердження все ще просто написати. Краще ви можете стверджувати, що вміст списку рівний, навіть якщо клас елементів не замінює, equals()/hashCode()тоді як способи JUnit вимагають, щоб:

import org.assertj.core.api.Assertions;
import static org.assertj.core.groups.Tuple.tuple;
import org.junit.jupiter.api.Test;

@Test
void ofComponent() throws Exception {
    MyFooObject myObject = MyFooObject.ofComponents(new Foo(1, "One"), new Foo(2, "Two"), new Foo(3, "Three"));

    Assertions.assertThat(myObject.getComponents())
              .extracting(Foo::getId, Foo::getName)
              .containsExactly(tuple(1, "One"),
                               tuple(2, "Two"),
                               tuple(3, "Three"));
}

1
  • Моя відповідь про те, чи Iterables.elementsEqualнайкращий вибір:

Iterables.elementsEqualдосить порівняти 2 Listс.

Iterables.elementsEqualвикористовуються в більш загальних сценаріях, він приймає більш загальні тип: Iterable. Тобто, ви навіть можете порівняти a Listз aSet . (за повторним замовленням це важливо)

Звичайно, ArrayListі LinkedListвизначити рівне досить добре, ви можете назвати рівне безпосередньо. Якщо ви використовуєте не чітко визначений список, Iterables.elementsEqualце найкращий вибір. Слід зауважити одне: Iterables.elementsEqualне приймаєnull

  • Для перетворення списку в масив: Iterables.toArrayпростіше.

  • Для одиничного тесту рекомендую додати порожній список до свого тесту.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.