Вызов генератора FsCheck по умолчанию из пользовательского генератора того же типа

Я определил тип с несколькими пользовательскими генераторами, чтобы FsCheck генерировал настраиваемые экземпляры нескольких типов. Но для одного из сложных типов я хочу сначала использовать генерацию FsCheck по умолчанию, а затем скорректировать результат. Вот (упрощенный) код:

type CustomGenerators =
    static member FirstCustomType() = /* function that returns FirstCustomType */
    static member SecondCustomType() =
        Arb.generate<SecondCustomType>
        |> Gen.map (fun x -> /* adjust some data in the generated instance */)
        |> Arb.fromGen

Проблема в том, что когда статический метод SecondCustomType() вызывает Arb.generate, он немедленно вызывает SecondCustomType(), вызывая бесконечную рекурсию. Я понимаю, что Arb.generate должен учитывать пользовательские генераторы, поэтому он вызывает статический SecondCustomType(), но мне нужно вызвать реализацию Arb.generate по умолчанию (не настроенную) для SecondCustomType. Я не могу вызвать реализацию из другого типа, потому что мой пользовательский генератор использует пользовательский генератор для FirstCustomType, поэтому реализация SecondCustomType по умолчанию должна знать обо всех пользовательских генераторах, определенных в типе CustomGenerators. Это своего рода плохой круг, для которого я еще не нашел четкого решения (только обходной путь).


person Vagif Abilov    schedule 01.04.2016    source источник
comment
Я бы предложил обернуть SecondCustomType в тривиальную оболочку только для теста, скажем, SecondCustomTypeTestWrapper, определить собственный генератор для этой оболочки, а не для самого SecondCustomType, и пусть ваш тест использует оболочку в качестве параметра.   -  person Fyodor Soikin    schedule 01.04.2016
comment
Да, это то, что я уже сделал, но мне было интересно, есть ли лучший способ без дополнительной оболочки типа.   -  person Vagif Abilov    schedule 01.04.2016
comment
Вместо того, чтобы сразу определять произвольные значения в статических классах, не могли бы вы определить некоторые нормальные функции F#, которые возвращают значения gen, а затем составлять их по мере необходимости? Это то, что я обычно делаю, но я никогда не использую статические классы, основанные на соглашениях...   -  person Mark Seemann    schedule 01.04.2016
comment
Было бы легче дать конкретный ответ, если бы вы могли опубликовать небольшую репродукцию.   -  person Mark Seemann    schedule 01.04.2016
comment
Я пробовал это, но столкнулся со смертельной рекурсией. Я попытаюсь извлечь пример, чтобы проиллюстрировать, что происходит.   -  person Vagif Abilov    schedule 01.04.2016
comment
@MarkSeemann, возможно, я слишком привязался к технике, которую вы продемонстрировали в своем курсе, но я обнаружил, что метод получения из свойства пользовательского атрибута и предоставления пользовательских генераторов в пользовательском типе очень эффективен. Похоже, что предложение Федора сработало хорошо - использование Arb.Default.Derive помогло, и я больше не получаю бесконечные рекурсивные вызовы, вызванные Arb.generate.   -  person Vagif Abilov    schedule 04.04.2016


Ответы (1)


Все генераторы «по умолчанию» (т. е. поставляемые «из коробки») находятся на FsCheck.Arb.Default. В зависимости от того, что на самом деле представляет собой ваш SecondCustomType, вы можете использовать некоторые методы этого класса, такие как Bool или String.

Если ваш тип является правильным алгебраическим типом F# (т. е. объединением, записью или кортежем), вы можете воспользоваться автоматическим генератором для таких типов, который представлен Default.Derive.

type CustomGenerators =
    static member SecondCustomType() =
        Arb.Default.Derive<SecondCustomType>()
        |> Arb.toGen
        |> Gen.map (fun x -> (* adjust some data in the generated instance *) )
        |> Arb.fromGen

Сказав это, я бы согласился с комментарием Марка выше: использование этих генераторов статического метода-прокладки-для-типа-класса всегда будет несколько неудобным. Как и Марк, я предпочитаю, чтобы FsCheck предоставлял то, что он может предоставить из коробки, а затем формировал желаемый ввод с помощью обычных функций. Я приведу вам пример.

Рассмотрим такой тип, который, предположительно, не может быть сгенерирован FsCheck из коробки:

type SomeAwkwardType( name: string, id: int, flag: bool ) =
   member this.Name = name
   member this.Id = id
   member this.Flag = flag

Вот неудобный способ использования генератора статических прокладок для типовых классов:

type AwkwardTypeGenerator() =
   static member Gen() =
      gen {
         let! name = Arb.generate<string>
         let! id = Arb.generate<int>
         let! flag = Arb.generate<bool>
         return SomeAwkwardType( name, id, flag )
      }

module Tests =
   let [Property] ``Test using the awkward generator`` (input: SomeAwkwardType) = 
      someFn input = 42

А вот более простой (на мой взгляд) способ создания ввода:

module Tests =
   let [Property] ``Test using straightforward generation`` (name, id, flag) = 
      let input = SomeAwkwardType( name, id, flag )
      someFn input = 42

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

person Fyodor Soikin    schedule 01.04.2016
comment
Спасибо, попробую позже, может, завтра. - person Vagif Abilov; 01.04.2016
comment
Это сработало идеально! Arb.Default.Derive сделал свое дело, поэтому я смог внести коррективы в сгенерированные значения, не вызывая бесконечной рекурсии. Когда дело доходит до другого вашего предложения (с использованием более простого способа генерирования ввода), оно определенно выглядит чище в этом конкретном тесте. Но когда выбранный подход определяет пользовательский атрибут SomeAckwardTypePropertyAttribute, то возврат к [Property] и отправка в тесты наборов отдельных значений вместо экземпляра полученного типа также имеет свои недостатки. Поэтому я бы предпочел использовать Derive, как вы предложили. - person Vagif Abilov; 04.04.2016