Search code examples
f#fscheck

Using FsCheck to Generate Records


I'd like to use FsCheck (with XUnit) to create records of type: type QueryRequest = {Symbol: string; StartDate: DateTime; EndDate: DateTime} where Symbol is limited to 3 options - ORCL, IBM, AAPL, and StartDate and EndDate are limited to the range between January 1, 2000 and January 1, 2019.

However, I'm unclear as to how proceed. Should I use Arb.generate<T> or Arb.Default or some other utility upon which to base the generation and shrinking of the test cases?


Update 1

Follow-on question related to issues generating records is available here.

Original:
{ Symbol = ""
  StartDate = 8/9/2057 4:07:10 AM
  EndDate = 10/14/2013 6:15:32 PM }
Shrunk:
{ Symbol = ""
  StartDate = 8/9/2057 12:00:00 AM
  EndDate = 10/14/2013 12:00:00 AM }

Update 2

Following is test suite code:

namespace Parser

open Xunit
open FsCheck.Xunit
open DataGenerators

module Tests =
    [<Fact>]
    let ``sanity check`` () =
        let expected = true
        let actual = true
        Assert.Equal(expected, actual)

    [<Property(Arbitrary = [|typeof<StockTwitGenerator>|])>]
    let ``validate queries`` (q: QueryRecord) =
        q.EndDate > q.StartDate

Solution

  • When you have constraints that limit the values to a small subset of all allowed values for a given type, constructing a valid value is easier and more safe1 than filtering.

    Given...

    open FsCheck
    open System
    
    type QueryRequest = {Symbol: string; StartDate: DateTime; EndDate: DateTime}
    

    ... we can start by creating a generator for Symbols:

    let symbols = ["ORCL"; "IBM"; "AAPL"]
    let symbol = Gen.elements symbols
    

    and a date range

    let minDate = DateTime(2000, 1, 1)
    let maxDate = DateTime(2019, 1, 1)
    let dateRange = maxDate - minDate
    let date =
        Gen.choose (0, int dateRange.TotalDays)
        |> Gen.map (float >> minDate.AddDays)
    

    Note that Gen.choose only accepts an int range. We can work around by generating a random offset of at max the allowed date difference and then mapping back to a DateTime

    Using those, we can construct a generator for QueryRequests...

    let query =
        gen {
            let! s = symbol
            let! d1 = date
            let! d2 = date
            let startDate, endDate = if d1 < d2 then d1, d2 else d2, d1 
            return { Symbol = s; StartDate = startDate; EndDate = endDate }
        }
    
    type MyGenerators =
      static member QueryRequest() =
          {new Arbitrary<QueryRequest>() with
              override _.Generator = query }
    

    ... register ...

    Arb.register<MyGenerators>()
    

    and finally test:

    let test { Symbol = s; StartDate = startDate; EndDate = endDate } =
        symbols |> Seq.contains s && startDate >= minDate && endDate <= maxDate && startDate <= endDate
    
    Check.Quick test
    

    1 FsCheck Documentation

    Make sure there is a high chance that the predicate is satisfied.