Как использовать генератор FsCheck для создания двух записей одного типа, где свойство одной записи отличается от другого

У меня есть этот тест fscheck nunit, который генерирует две записи, которые мне затем нужно обновить, чтобы обе записи всегда имели разные значения для свойства Direction.

[<Property( Verbose = true )>]
let ``calculate Net Worth 2`` (first:Bill,second:Bill) =
  let owing = { first with Direction = Out }
  let payCheck = { second with Direction = In }

  let compositeBill = {
    Bills = [| owing; payCheck |] 
  }
  let netWorth = calculateNetWorth compositeBill
  Assert.AreEqual(payCheck.Amount - owing.Amount,netWorth)

Я не хочу вручную устанавливать Direction = In или Direction = In , я хочу использовать генератор, чтобы указать это.

Как будет выглядеть такой генератор?

Я хочу, чтобы у меня остался такой код

[<Property( Verbose = true )>]
let ``calculate Net Worth 2`` (first:Bill,second:Bill) =
  let compositeBill = {
    Bills = [| owing; payCheck |] 
  }
  let netWorth = calculateNetWorth compositeBill
  Assert.AreEqual(payCheck.Amount - owing.Amount,netWorth)

Вот что я пробовал без везения

type BillsGen =
    static member Bill () =        
        let debit = 
          Arb.generate<Bill>
          |> Gen.map (fun dt -> { dt with Direction = Out} )          
        let credit = 
          Arb.generate<Bill>
          |> Gen.map (fun dt -> { dt with Direction = In} )
        Gen.oneof[ debit; credit ]        

[<SetUp>]
let setup () =
    do Arb.register<BillsGen>() |> ignore 

Спасибо

Вот некоторые из моих типов

   type Direction = 
    | In  
    | Out

   type Bill = {
     Direction : Direction
   }

   type CompositeBill = {
    Bills : Bill [] 
   }

person Samuel    schedule 12.06.2017    source источник


Ответы (2)


У меня нет опыта тестирования на основе свойств, но я думаю, что вы сделаете это, создав новый тип, который представляет ваше ограниченное входное значение. Затем вы можете использовать Gen.zip в своем генераторе, чтобы объединить два генератора для создания значения этого типа.

type BillsInOut = BillsInOut of Bill * Bill

type BillsInOutGen =
    static member BillsInOut () =
        { new Arbitrary<BillsInOut>() with
            override x.Generator =
                let credit =
                    Arb.generate<Bill>
                    |> Gen.map (fun dt -> { dt with Direction = In })
                let debit =
                    Arb.generate<Bill>
                    |> Gen.map (fun dt -> { dt with Direction = Out })

                Gen.zip credit debit |> Gen.map BillsInOut }

И как только вы запустите Arb.register<BillsInOutGen>(), вы можете взять этот новый тип в качестве тестового аргумента, и это свойство должно содержать:

let property (BillsInOut (inBill, outBill)) =
    inBill.Direction = In && outBill.Direction = Out

Изменить: другой подход

Мне просто пришло в голову, что, поскольку эти два счета независимы, их можно генерировать отдельно, поэтому нет необходимости объединять генераторы вместе, и вы можете получать разные типы по мере необходимости:

type BillIn = BillIn of Bill
type BillOut = BillOut of Bill

type BillInOutGen =
    static member BillIn () =
        { new Arbitrary<BillIn>() with
            override x.Generator =
                Arb.generate<Bill> |> Gen.map (fun dt -> BillIn { dt with Direction = In }) }
    static member BillOut () =
        { new Arbitrary<BillOut>() with
            override x.Generator =
                Arb.generate<Bill> |> Gen.map (fun dt -> BillOut { dt with Direction = Out }) }

Arb.register<BillInOutGen>()

Свойство, которое их использует, теперь выглядит так:

let property (BillIn inBill) (BillOut outBill) =
    inBill.Direction = In && outBill.Direction = Out
person TheQuickBrownFox    schedule 12.06.2017
comment
Благодарю вас! Используя ваш первый подход, мы упускаем что-то вроде типа BillsInOut = BillsInOut of Bill * Bill ? - person Samuel; 14.06.2017
comment
Починил это. Спасибо. - person TheQuickBrownFox; 15.06.2017

Ответ @thequickbrownfox был очень полезен. Но мне пришлось внести некоторые изменения в тесты. Мне пришлось создать тип BillsInOut = BillsInOut of Bill * Bill и указать классы для проведения моих тестов, и оттуда все работало Решение показано ниже

type BillsInOut = BillsInOut of Bill * Bill

type BillsInOutGen =
  static member BillsInOut () =
    { 
     new Arbitrary<BillsInOut>() with
      override x.Generator =
        let credit =
          Arb.generate<Bill>
          |> Gen.map (fun dt -> { dt with Direction = In })
        let debit =
          Arb.generate<Bill>
          |> Gen.map (fun dt -> { dt with Direction = Out })        
        Gen.zip credit debit |> Gen.map BillsInOut 
    }


[]
type ``when analysing bills``() =

  [<SetUp>]
  member x.SetUp() = 
   Arb.register<BillsInOutGen>() |> ignore

  [<Property( Verbose = true )>]
  member x.``it should calculate net worth`` (BillsInOut (payCheck, owing)) = 
    Assert.True(payCheck.Direction = In && owing.Direction = Out)       

Решение о создании двух счетов по отдельности не сработало.

person Samuel    schedule 14.06.2017