testingf#fscheck

How do I create an Arbitrary for a System.Type?


I am attempting to initialize my model entities using FsCheck. The models live in C# and are normally initialized via Entity Framework via their private setters. For example (contrived):

public class Model
{
    public string One { get; private set; } 
    public int Two { get; private set; } 
}

I would like to create an FsCheck generator that automatically uses the registered generator for each property to produce the model. Something like this:

let modelGenerator = 
    gen {
        let incident = new Model()
        typeof<Model>.GetProperties()
            |> Array.filter (fun p -> p.CanWrite)
            |> Array.iter (fun p -> 
                let! newVal = Arb.generateType p.PropertyType // I wish I could do this
                p.SetValue(incident, newVal))

        return incident
    }

There are two things wrong with this:

  1. The let! can't be used outside of the gen computation expression.
  2. Arb.generateType doesn't exist, and I can't find a way to do its equivalent

Is it possible to create a generator that will automatically set the private fields on my model?


Solution

  • With the power of reflection, everything is possible (or throws at runtime).

    module Arb = 
        open System.Reflection 
    
        // this is just a helper type to do reflection on. 
        type internal GenerateInvoker = 
            static member Invoke<'typ> () = 
                Arb.generate<'typ>
                |> Gen.map box
    
        // Invokes a generic method using a runtime type as a generic argument.
        let generateType (typ: Type) =
            typeof<GenerateInvoker>
                .GetMethod("Invoke", BindingFlags.Static ||| BindingFlags.NonPublic)
                .MakeGenericMethod([|typ|])
                .Invoke(null, [||]) :?> Gen<obj>
    
    let modelGenerator = 
        gen {
            let incident = new Model()
            let props =  
                typeof<Model>.GetProperties()
                |> Array.filter (fun p -> p.CanWrite)
    
            // gen builder implements For, so you can do something like this. 
            for prop in props do 
                let! newVal = Arb.generateType prop.PropertyType
                prop.SetValue(incident, newVal)
    
            return incident
        }
    
    Gen.sample 1 3 modelGenerator