3
votes

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
2
it doesn’t quite work is not a good problem description. I’d remove your update and ask a separate question.CaringDev
@CaringDev agreed; separated follow-on question. Link to follow-on provided in original post update.Ari

2 Answers

3
votes

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

3
votes

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.