Воспроизводимо ли использовать 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 может позволить передать текущее состояние при генерации действий. Это сделало бы ваши вещи немного проще. Но эта функция пока не в приоритете.

Другие вопросы по тегам