Как проверить ограничения идентичности и ассоциативности пользовательского сборщика java-8

Я написал пользовательский коллектор для Java 8. Его агрегатор представляет собой карту, содержащую пару списков:

    @Override
    public Supplier<Map<Boolean, List<Object>>> supplier() {
        return () -> {
            Map<Boolean, List<Object>> map = new HashMap<>(2);
            map.put(false, new ArrayList<>());
            map.put(true, new ArrayList<>());
            return map;
        };
    }

поэтому я думаю, что его объединитель таков:

    @Override
    public BinaryOperator<Map<Boolean, List<Object>>> combiner() {
        return (a, b) -> {
            a.get(false).addAll(b.get(false));
            a.get(true).addAll(b.get(true));
            return a;
        };
    }

Я хотел бы проверить коллектор, чтобы убедиться, что если и когда он обрабатывает поток параллельно, результат будет правильным.

как я могу написать модульный тест, упражнения для этого?

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

Javadoc для Collector говорит:

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

могу ли я добиться доверия к своему коллекционеру, Протестировав эти ограничения? Как?

3 ответов


вы в основном спрашиваете, если List.addAll ассоциативно. Потому что тождество тривиально решается Object.equals, которому гарантирует следовать каждая стандартная коллекция (которую вы используете).

ассоциативность

ассоциативность означает следующее:

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

(2 + 3) + 4 = 2 + (3 + 4) = 9
2 × (3 × 4) = (2 × 3) × 4 = 24

-- Википедия

да List.addAllS является ассоциативной.

покажем это на примере:

import java.util.*;
public class Main {
  // Give addAll an operator look.
  static <T> List<T> myAddAll(List<T> left, List<T> right) {
    List<T> result = new ArrayList<>(left);
    result.addAll(right);
    return result;
  }
  public static void main(String[] args) {
    List<Integer> a = Arrays.asList(1, 2, 3);
    List<Integer> b = Arrays.asList(4, 5, 6);
    List<Integer> c = Arrays.asList(7, 8, 9);

    // Combine a and b first, then combine the result with c.
    System.out.println(myAddAll(myAddAll(a, b), c)); // [1, 2, 3, 4, 5, 6, 7, 8, 9]

    // Combine b and c first, then combine a with the result.
    System.out.println(myAddAll(a, myAddAll(b, c))); // [1, 2, 3, 4, 5, 6, 7, 8, 9]
  }
}

тестирование

контракт Collector - это именно то, что вы написали: убедитесь, что комбайнер был оба личность и ассоциативность свойства. Если вы будете следовать этому, вы не получите никаких проблем (убедитесь, что вы намекаете, что ваш Spliterator is ORDERED при необходимости, конечно).

тест затем сводится к тому, чтобы просто проверить, что ваш объединитель имеет эти два свойства. The личность часть гарантирована равными,ассоциативность часть обрабатывается путем написания теста, аналогичного приведенному выше коду. Она кипит вплоть до этого, потому что, как сказал @mrmcgreg в комментариях, вы не должны тестировать саму структуру: это ответственность авторов Java. Если вы столкнулись с какими-либо проблемами после того, как доказали, что ваш объединитель выполняет два свойства, вы, вероятно, должны подать ошибку на Java.


С благодарностью обоим ответчикам, которые послали меня по тому, что я считаю правильным путем.

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

T result = myList.stream().parallel().collect(myCollector);

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

поэтому тестирование контракта похоже на путь. Доверие Stream.collect() сделать правильный дали Collector это работает. Это обычная практика, чтобы не тестировать" предоставленные " библиотеки.


на Collector JavaDoc определяет ограничения и даже предоставляет код, описывающий ограничение ассоциативности. Мы можем вытащить этот код в класс тестирования, который можно использовать в реальном мире:

public class CollectorTester<T, A, R> {

    private final Supplier<A> supplier;
    private final BiConsumer<A, T> accumulator;
    private final Function<A, R> finisher;
    private final BinaryOperator<A> combiner;

    public CollectorTester(Collector<T, A, R> collector) {
        this.supplier = collector.supplier();
        this.accumulator = collector.accumulator();
        this.combiner = collector.combiner();
        this.finisher = collector.finisher();
    }

    // Tests that an accumulator resulting from the inputs supplied
    // meets the identity constraint
    public void testIdentity(T... ts) {
        A a = supplier.get();
        Arrays.stream(ts).filter(t -> t != null).forEach(
            t -> accumulator.accept(a, t)
        );

        assertThat(combiner.apply(a, supplier.get()), equalTo(a));
    }

    // Tests that the combiner meets the associativity constraint
    // for the two inputs supplied
    // (This is verbatim from the Collector JavaDoc)
    // This test might be too strict for UNORDERED collectors
    public void testAssociativity(T t1, T t2) {
        A a1 = supplier.get();
        accumulator.accept(a1, t1);
        accumulator.accept(a1, t2);
        R r1 = finisher.apply(a1); // result without splitting

        A a2 = supplier.get();
        accumulator.accept(a2, t1);
        A a3 = supplier.get();
        accumulator.accept(a3, t2);
        R r2 = finisher.apply(combiner.apply(a2, a3)); // result with splitting

        assertThat(r1, equalTo(r2));
    }

}

остается проверить это с достаточным диапазоном входов. Один из способов достичь этого-с JUnit 4's Theories бегун. Например, для тестирования Collectors.joining():

@RunWith(Theories.class)
public class MaxCollectorTest {

    private final Collector<CharSequence, ?, String> coll = Collectors.joining();
    private final CollectorTester<CharSequence, ?, String> tester = new CollectorTester<>(coll);

    @DataPoints
    public static String[] datapoints() {
        return new String[] { null, "A", "rose", "by", "any", "other", "name" };
    }

    @Theory
    public void testAssociativity(String t1, String t2) {
        assumeThat(t1, notNullValue());
        assumeThat(t2, notNullValue());
        tester.testAssociativity(t1, t2);
    }

    @Theory
    public void testIdentity(String t1, String t2, String t3) {
        tester.testIdentity(t1, t2, t2);
    }
}

(мне приятно, что мой тестовый код не должен знать тип Collectors.joining()аккумулятор (который не объявлен API) для этого теста для работы)


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


Оливье ответил на часть ассоциативности / идентичности.

Что касается тестов, вы можете либо приготовить свои собственные тестовые случаи, которые, надеюсь, охватывают все угловые случаи, или попробовать тестирование на основе свойств ala Haskell QuickCheck (Java имеет QuickTheories, например).

Что это будет сделать, это создать кучу случайных объектов, и посмотреть, если объявить для всех из них, когда вы применить ваш оператор. Более крутая кривая обучения, чтобы получить но стоит после этого :)