Как проверить ограничения идентичности и ассоциативности пользовательского сборщика 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.addAll
S является ассоциативной.
покажем это на примере:
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, например).
Что это будет сделать, это создать кучу случайных объектов, и посмотреть, если объявить для всех из них, когда вы применить ваш оператор. Более крутая кривая обучения, чтобы получить но стоит после этого :)