3

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
Ari
  • 4,121
  • 8
  • 40
  • 56
  • `it doesn’t quite work` is not a good problem description. I’d remove your update and ask a separate question. – CaringDev Oct 30 '19 at 08:51
  • 1
    @CaringDev agreed; separated follow-on question. Link to follow-on provided in original post update. – Ari Oct 31 '19 at 00:09

2 Answers2

3

Arb.filter filters the generator and shrinker for a given Arbitrary instance to contain only those values that match with the given filter function. This should help you meet your needs.

https://fscheck.github.io/FsCheck/TestData.html#Useful-methods-on-the-Arb-module https://github.com/fscheck/FsCheck/blob/master/src/FsCheck/ArbitraryExtensions.fs#L17-17

VoronoiPotato
  • 3,113
  • 20
  • 30
3

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.

CaringDev
  • 8,391
  • 1
  • 24
  • 43
  • How did you know that `gen` would accept a computation expression? – Ari Oct 30 '19 at 07:48
  • Thanks, in original read through I skipped down to examples. For others' future reference the explanation re: computation expressions starts in paragraph two of the provided link. – Ari Oct 31 '19 at 00:11
  • When executing test cases with the generator you provided as an example above, the cases include erroneous/malformed records (see update in post for examples) that have missing symbols, and end dates that precede start dates. Any ideas on what could be the issue? – Ari Oct 31 '19 at 00:41
  • How are you executing the tests? Are you sure you registered the generators? When I execute the above, even for 10s of 1000s of executions, I don't get wrong data... – CaringDev Oct 31 '19 at 05:13
  • I updated code to include test suite. Essentially, I was executing `dotnet test` with the expectation that it would run all identified tests. This happened as expected, but apparently the issue was a default arbitrary instance was used rather than the custom "StockTwitGenerator". To resolve this issue, I specified the arbitrary value in the property attribute (see update 2). I had to specify this value irrespective of whether I registered generator i.e. `Arb.register() |> ignore`. – Ari Nov 01 '19 at 00:23
  • To your knowledge is there a way to configure FsCheck to use the custom arbitrary as a primary, aside from specifying it in a property attribute, with the default as a fallback? – Ari Nov 01 '19 at 00:24
  • You could use xUnit CollectionFixture or a constructor. I also tried with module initializers but failed :-/ – CaringDev Nov 02 '19 at 19:10