Тест Junit завершается неудачно после обмена реализацией с потоковым API, почему? - PullRequest
6 голосов
/ 17 апреля 2019

Я реализовал следующий метод, который обеспечивает обзор более String с и их появления в значениях Map<String, List<String>>:

public static Map<String, Long> getValueItemOccurrences(Map<String, List<String>> map) {
    Map<String, Long> occurrencesOfValueItems = new HashMap<>();

    map.forEach((key, value) -> {
        value.forEach(item -> {
            if (occurrencesOfValueItems.containsKey(item)) {
                occurrencesOfValueItems.put(item, occurrencesOfValueItems.get(item) + 1);
            } else {
                occurrencesOfValueItems.put(item, 1L);
            }
        });
    });

    return occurrencesOfValueItems;
}

Я проверил его одним тестом JUnit, и тест прошел успешно. Вот оно ( теперь также включает импорт ):

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;

class TryoutTest {

    static Map<String, List<String>> items = new HashMap<>();
    static List<String> largeList = new ArrayList<String>();
    static List<String> mediumList = new ArrayList<String>();       
    static List<String> smallList = new ArrayList<String>();
    static List<String> differentLargeList = new ArrayList<String>();
    static List<String> differentSmallList = new ArrayList<String>();
    static List<String> anotherList = new ArrayList<String>();
    static List<String> someList = new ArrayList<String>();
    static List<String> justAList = new ArrayList<String>();

    @BeforeAll
    static void setup() {
        largeList.add("Alfred");
        largeList.add("Bakari");
        largeList.add("Christian");
        largeList.add("Dong");
        largeList.add("Etienne");
        largeList.add("Francesco");
        largeList.add("Guido");
        largeList.add("Henrik");
        largeList.add("Ivan");
        largeList.add("Jos");
        largeList.add("Kumar");
        largeList.add("Leonard");
        largeList.add("Marcin");
        largeList.add("Nico");
        largeList.add("Olof");
        items.put("fifteen-01", largeList);

        mediumList.add("Petar");
        mediumList.add("Quentin");
        mediumList.add("Renato");
        mediumList.add("Sadio");
        mediumList.add("Tomislav");
        mediumList.add("Ulrich");
        mediumList.add("Volkan");
        mediumList.add("Wladimir");
        items.put("eight-01", mediumList);

        smallList.add("Xavier");
        smallList.add("Yves");
        smallList.add("Zinedine");
        smallList.add("Alfred");
        items.put("four-01", smallList);

        differentLargeList.add("Bakari");
        differentLargeList.add("Christian");
        differentLargeList.add("Dong");
        differentLargeList.add("Etienne");
        differentLargeList.add("Francesco");
        differentLargeList.add("Xavier");
        differentLargeList.add("Yves");
        differentLargeList.add("Wladimir");
        differentLargeList.add("Jens");
        differentLargeList.add("Hong");
        differentLargeList.add("Le");
        differentLargeList.add("Leigh");
        differentLargeList.add("Manfred");
        differentLargeList.add("Anders");
        differentLargeList.add("Rafal");
        items.put("fifteen-02", differentLargeList);

        differentSmallList.add("Dario");
        differentSmallList.add("Mohammad");
        differentSmallList.add("Abdul");
        differentSmallList.add("Alfred");
        items.put("four-02", differentSmallList);

        anotherList.add("Kenneth");
        anotherList.add("Hong");
        anotherList.add("Bakari");
        anotherList.add("Ulrich");
        anotherList.add("Henrik");
        anotherList.add("Bernd");
        anotherList.add("Samuel");
        anotherList.add("Ibrahim");
        items.put("eight-02", anotherList);

        someList.add("Kumar");
        someList.add("Konrad");
        someList.add("Bakari");
        someList.add("Francesco");
        someList.add("Leigh");
        someList.add("Yves");
        items.put("six-01", someList);

        justAList.add("Bakari");
        items.put("one-01", justAList);
    }

    @Test
    void valueOccurrencesTest() {
        Map<String, Integer> expected = new HashMap<>();
        expected.put("Abdul", 1);
        expected.put("Alfred", 3);
        expected.put("Anders", 1);
        expected.put("Bakari", 5);
        expected.put("Bernd", 1);
        expected.put("Christian", 2);
        expected.put("Dario", 1);
        expected.put("Dong", 2);
        expected.put("Etienne", 2);
        expected.put("Francesco", 3);
        expected.put("Guido", 1);
        expected.put("Henrik", 2);
        expected.put("Hong", 2);
        expected.put("Ibrahim", 1);
        expected.put("Ivan", 1);
        expected.put("Jens", 1);
        expected.put("Jos", 1);
        expected.put("Kenneth", 1);
        expected.put("Konrad", 1);
        expected.put("Kumar", 2);
        expected.put("Le", 1);
        expected.put("Leigh", 2);
        expected.put("Leonard", 1);
        expected.put("Manfred", 1);
        expected.put("Marcin", 1);
        expected.put("Mohammad", 1);
        expected.put("Nico", 1);
        expected.put("Olof", 1);
        expected.put("Petar", 1);
        expected.put("Quentin", 1);
        expected.put("Rafal", 1);
        expected.put("Renato", 1);
        expected.put("Sadio", 1);
        expected.put("Samuel", 1);
        expected.put("Tomislav", 1);
        expected.put("Ulrich", 2);
        expected.put("Volkan", 1);
        expected.put("Wladimir", 2);
        expected.put("Xavier", 2);
        expected.put("Yves", 3);
        expected.put("Zinedine", 1);
        assertThat(FunctionalMain.getValueItemOccurrences(items), is(expected));
    }
}

Когда я меняю реализацию метода на

public static Map<String, Long> getValueItemOccurrences(Map<String, List<String>> map) {
    return map.values().stream()
            .flatMap(Collection::stream)
            .collect(Collectors.groupingBy(Function.identity(), Collectors.counting()));
}

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

enter image description here

Это действительно так? Я думаю, что прочитал, что HashMap s, как правило, не гарантирует какой-либо порядок ключей.

Мой (довольно длинный) вопрос: что я могу сделать, чтобы потоковый API-вызов выдал результат, который проходит тест, или мне нужно изменить тестовый пример, возможно, использовать другое утверждение?

Некоторые подвопросы:

  • существует ли альтернативный / лучший способ использования потокового API для этого метода?
  • Должен ли я возвращать конкретную реализацию Map, если порядок имеет значение (TreeMap, может быть)?

Ответы [ 2 ]

5 голосов
/ 18 апреля 2019

TL; DR ваш тест не пройден, исправьте это.

Прежде всего, это легче воспроизвести с помощью:

List<String> list = ImmutableList.of("Kumar", "Kumar", "Jens");

public static Map<String, Long> getValueItemOccurrences1(List<String> list) {
    return list
        .stream()
        .collect(Collectors.groupingBy(Function.identity(), Collectors.counting()));
}

public static Map<String, Long> getValueItemOccurrences2(List<String> list) {
    Map<String, Long> occurrencesOfValueItems = new HashMap<>();

    list.forEach(item -> {
        if (occurrencesOfValueItems.containsKey(item)) {
            occurrencesOfValueItems.put(item, occurrencesOfValueItems.get(item) + 1);
        } else {
            occurrencesOfValueItems.put(item, 1L);
        }
    });

    return occurrencesOfValueItems;
}

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

    System.out.println(hash("Kumar".hashCode()) & 15);
    System.out.println(hash("Jens".hashCode()) & 15);

Проще говоря, HashMap решает, куда поместить запись (выбран контейнер) на основе hashCode ваших записей. Ну, почти, как только вычисляется hashCode, внутренне делается еще один hash - для лучшего распределения записей. Это окончательное int значение hashCode используется для определения сегмента. Когда вы создаете HashMap с емкостью по умолчанию 16 (например, через new HashMap), только последние 4 бита имеют значение, куда будет входить запись (вот почему я сделал & 15 там - чтобы увидеть последние 4 биты).

, где hash:

// xor first 16 and last 16 bits
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

Теперь оказывается, что ["Kumar" and "Jens"] или ["Xavier", "Kenneth", "Samuel"] имеют те же последние 4 цифры после применения алгоритма, описанного выше (3 в первом случае и 1 во втором случае).

Теперь, когда мы знаем эту информацию, это на самом деле можно упростить еще больше:

Map<String, Long> map = new HashMap<>();
map.put("Kumar", 2L);
map.put("Jens", 1L);

System.out.println(map); // {Kumar=2, Jens=1}

map = new HashMap<>();
map.computeIfAbsent("Kumar", x -> 2L);
map.computeIfAbsent("Jens", x -> 1L);
System.out.println(map); // {Jens=1, Kumar=2}

Я использовал map.computeIfAbsent, потому что это то, что Collectors.groupingBy использует под капотом.


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


Это даже забавное чтение, если хотите:

HashMap::put будет добавлять элементы Linked способом (до тех пор, пока не будут созданы Tree записи), поэтому, если у вас есть один элемент, все остальные будут добавлены как:

one --> next --> next ... so on.

элементы добавляются к end of this queue по мере их поступления в метод put.

С другой стороны computeIfAbsent немного отличается, он добавляет элементы в начало очереди. Если взять приведенный выше пример, сначала добавляется Xavier. Затем, когда добавляется Kenneth, становится первым:

 Kenneth -> Xavier // Xavier was "first"

Когда добавлено Samuel, оно становится первым:

 Samuel -> [Kenneth -> Xavier] 
0 голосов
/ 19 апреля 2019

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

assertThat(FunctionalMain.getValueItemOccurrences(items))containsOnly(expected);

containsOnly() проверяет, что ваши карты имеют одинаковые элементы в любом порядке

Помимо этого преимущества, assertThat() в AssertJ также использует гибкий синтаксис (в отличие от встроенного в JUnit assertThat), так что ваша IDE может предоставить контекстно-зависимую помощь, чтобы вы знали, какое из сотен утверждений AssertJ доступно для типа. ваше проверенное значение.

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