Воспроизводимо ли использовать Arbitrary.sample из Action?
У нас есть проверка состояния системы заказов. Существует
Arbitrary
что создаст
Order
объект, который имеет ряд
LineItem
с.
Есть действия, чтобы:
- Создать
Order
- Отменить
LineItem
Действие по созданию заказа принимает сам заказ, например:
Arbitraries.defaultFor(Order.class).map(CreateOrderAction::new)
Государству для действий известны все созданные заказы.
Чтобы отменить
LineItem
, нам необходимо знать, какие заказы создаются. Внутри
CancelLineItemAction
безопасно ли делать следующее?
LineItem line = Arbitraries.<Collection<Order>>of(state.orders())
.flatMap(order -> Arbitraries.<Collection<LineItem>>of(order.lineItems()))
.sample();
На основе javadoc
Arbitrary.sample()
, это кажется безопасным, но эта конструкция явно не упоминается в документации по тестам с отслеживанием состояния, и мы не хотим широко использовать ее только для нарушения воспроизводимости наших тестов.
1 ответ
TL; DR
Arbitrary.sample()
не предназначен для использования таким образом- Я рекомендую использовать индекс случайной отмены с модулем количества позиций
1. Почему Arbitrary.sample() не рекомендуется
Arbitrary.sample()
предназначен для использования вне свойств, например, для экспериментов со сгенерированными значениями или для использования в других контекстах, таких как JUnit Jupiter. Причин как минимум три:
- Базовое случайное начальное число, используемое для генерации значений, зависит от того, что происходит перед выборкой. Таким образом, результаты на самом деле не воспроизводятся.
- Выборка не будет учитывать любые добавленные контексты домена, которые могут изменить то, что создается.
- Ценности, созданные
sample()
НЕ УЧАСТВУЙТЕ В УСИЛЕНИИ
2. Вариант 1. Передайте случайный объект и используйте его для создания
Передайте случайный экземпляр при генерации CancelLineItemAction:
Arbitraries.random().map(random -> new CancelLineItemAction(random))
Используйте случайное число, чтобы вызвать генератор:
LineItem line = Arbitraries.of(state.orders())
.flatMap(order -> Arbitraries.of(order.lineItems()))
.generator(100).next(random).value();
Но на самом деле это очень важно для того, чем вы хотите заниматься. Вот упрощение:
3. Вариант 2. Передайте случайный объект и используйте его для выбора позиции.
То же, что и выше, но не объезжайте с выборкой:
List<LineItem> lineItems = state.orders().stream()
.flatMap(order -> order.lineItems().stream())
.collect(Collectors.toList());
int randomIndex = random.nextInt(lineItems.size());
LineItem line = lineItems.get(randomIndex);
Оба варианта 1 и 2 будут (надеюсь) вести себя разумно в жизненном цикле jqwik, но они не будут пытаться сжать его. Поэтому рекомендую следующий вариант.
4. Вариант 3. Сдайте индекс отмены и умножьте его на количество позиций.
Чтобы сгенерировать действие:
Arbitraries.integer().between(0, MAX_LINE_ITEMS)
.map(cancelIndex -> new CancelLineItemAction(cancelIndex))
Используйте это в действии:
List<LineItem> lineItems = state.orders().stream()
.flatMap(order -> order.lineItems().stream())
.collect(Collectors.toList());
int randomIndex = cancelIndex % lineItems.size();
LineItem line = lineItems.get(randomIndex);
Более подробно подход описан здесь: https://blog.johanneslink.net/2020/03/11/model-based-testing/
5. Перспективы на будущее
В более или менее отдаленном будущем jqwik может позволить передать текущее состояние при генерации действий. Это сделало бы ваши вещи немного проще. Но эта функция пока не в приоритете.