Как создать "сложный" объект в FsCheck?

Я хотел бы создать FsCheck Generator, чтобы генерировать экземпляры "сложного" объекта. Сложным я имею в виду существующий класс в С#, который имеет несколько дочерних свойств и коллекций. Эти свойства и коллекции, в свою очередь, должны иметь для них данные.

Представьте, что этот класс был назван Menu с дочерними коллекциями Dishes и Drinks (я делаю это так, чтобы игнорировать дрянной дизайн). Я хочу сделать следующее:

  • Создать переменное число Dishes и переменное число Drinks.
  • Создайте экземпляры Dish и Drink, используя API FsCheck, чтобы заполнить их свойства.
  • Задайте некоторые примитивные свойства экземпляра Menu, используя API FsCheck.

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

Существует хороший пример для создания записи, но на самом деле это генерирует только 3 значения одного и того же типа float.

Ответ 1

Это неплохая идея - на самом деле все дело в том, что вы в состоянии это сделать. Генераторы FsCheck полностью композиционны.

Обратите внимание, что если у вас есть неизменные объекты, конструкторы которых принимают примитивные типы, например, ваш напиток и тарелка, FsCheck может генерировать их из коробки (используя отражение)

let drinkArb = Arb.from<Drink>
let dishArb = Arb.from<Dish>

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

Это довольно быстро ломается - в вашем примере вы, вероятно, не хотите отрицательных целых чисел для количества напитков или количества блюд. Приведенный выше код будет генерировать отрицательные числа. Иногда это легко исправить, если ваш тип действительно просто оболочка какого-то типа вокруг другого типа, используя Arb.convert, например.

let drinksArb = Arb.Default.PositiveInt() |> Arb.convert (fun positive -> new Drinks(positive) (fun drinks -> drinks.Amount)

Вам нужно предоставить и от конверсий к Arb.convert и presto, новый произвольный экземпляр для Drinks, который поддерживает ваш инвариант. Другие инварианты, возможно, не так легко поддерживать, конечно.

После этого становится немного сложнее генерировать генератор и сушку одновременно с этими двумя частями. Всегда начинайте с генератора, а затем сжимается, если (когда) вам это нужно. Пример @simonhdickson выглядит разумно. Если у вас есть произвольные экземпляры выше, вы можете получить их генератор, вызвав .Generator.

let drinksGen = drinksArb.Generator

Как только у вас есть генераторы деталей (Drink and Dish), вы можете составить их вместе, поскольку @simonhdickson предлагает:

let menuGenerator =
    Gen.map3 (fun a b c -> Menu(a,b,c)) (Gen.listOf dishGenerator) (Gen.listOf drinkGenerator) (Arb.generate<int>)

Разделите и победите! В целом взгляните на то, что intellisense от Gen дает вам представление о том, как создавать генераторы.

Ответ 2

Может быть, лучший способ описать это, но я думаю, что это может сделать то, о чем вы думаете. Каждый из типов Drink/Dish может принимать дополнительные параметры, используя тот же стиль, что и menuGenerator делает

type Drink() =
    member m.X = 1

type Dish() =
    member m.Y = 2

type Menu(dishes:Dish list, drinks:Drink list, total:int) =
    member m.Dishes = dishes
    member m.Drinks = drinks
    member m.Total = total

let drinkGenerator = Arb.generate<unit> |> Gen.map (fun () -> Drink())
let dishGenerator = Arb.generate<unit> |> Gen.map (fun () -> Dish())
let menuGenerator =
    Gen.map3 (fun a b c -> Menu(a,b,c)) <| Gen.listOf dishGenerator <| Gen.listOf drinkGenerator <| Arb.generate<int>