Search code examples
c#unit-testingfscheckgenerative-testing

FsCheck in C#: generate a list of two dimension arrays with the same shape


Let's say I'm writing some code for video analysis. Here is a simplified version of a Video class:

public class Video
{
    public readonly int Width;
    public readonly int Height;
    public readonly List<int[,]> Frames;

    public Video(int width, int height, IEnumerable<int[,]> frames)
    {
        Width = width;
        Height = height;
        Frames = new List<int[,]>();
        foreach (var frame in frames)
        {
            if (frame.GetLength(0) != height || frame.GetLength(1) != width)
            {
                throw new ArgumentException("Incorrect frames dimensions");
            }
            Frames.Add(frame);
        }
    }
}

How do I make an Arbitrary<Video> and register it? How do I make a shrinker for that Arbitrary?

Tried this, couldn't understand how apply works:

public static Arbitrary<Video> Videos()
{
    var videoGen = Arb.Generate<PositiveInt>()
        .SelectMany(w => Arb.Generate<PositiveInt>(), (w, h) => new {w, h})
        .Apply( /* what is Gen<Func<a,b>> */);

    return videoGen.ToArbitrary();
}

Tried this, but couldn't plug the generator for list in here:

public static Arbitrary<Video> Videos()
{
    var videoGen = Arb.Generate<PositiveInt>()
        .SelectMany(w => Arb.Generate<PositiveInt>(), (w, h) => new Video(w, h, /* how to plug generator here? */));

    return videoGen.ToArbitrary();
}

Solution

  • Using Kurt Schelfthout's answer as a foundation, you can write an Arbitrary for the video class like this:

    public static class VideoArbitrary
    {
        public static Arbitrary<Video> Videos()
        {
            var genVideo = from w in Arb.Generate<PositiveInt>()
                           from h in Arb.Generate<PositiveInt>()
                           from arrs in Gen.ListOf(
                               Gen.Array2DOf<int>(
                                   h.Item,
                                   w.Item,
                                   Arb.Generate<int>()))
                           select new Video(w.Item, h.Item, arrs);
            return genVideo.ToArbitrary();
        }
    }
    

    You can use this in various ways.

    Plain vanilla FsCheck

    Here's how to use the Video Arbitrary with plain vanilla FsCheck, here hosted within an xUnit.net test case, which isn't required: you can host this in whichever process you prefer:

    [Fact]
    public void VideoProperty()
    {
        var property = Prop.ForAll(
            VideoArbitrary.Videos(),
            video =>
            {
                // Test goes here...
                Assert.NotNull(video);
            });
        property.QuickCheckThrowOnFailure();
    }
    

    Prop.ForAll is very useful for defining properties with custom Arbitraries. When you call QuickCheckThrowOnFailure, it's going to run the test for 'all' (by defailt: 100) values of the Video class.

    Untyped xUnit.net property

    You can also use the FsCheck.Xunit Glue Library, but you have to pass the Arbitrary as a weakly typed value to the attribute:

    [Property(Arbitrary = new[] { typeof(VideoArbitrary) })]
    public void XunitPropertyWithWeaklyTypedArbitrary(Video video)
    {
        // Test goes here...
        Assert.NotNull(video);
    }
    

    This is simple and easy to understand, but there's no static type checking involved when assigning that Arbitrary property, so I'm not too fond of this approach.

    Typed xUnit.net property

    A better way to use FsCheck.Xunit with custom Arbitraries is to combine it with Prop.ForAll:

    [Property]
    public Property XUnitPropertyWithStronglyTypedArbitrary()
    {
        return Prop.ForAll(
            VideoArbitrary.Videos(),
            video =>
            {
                // Test goes here...
                Assert.NotNull(video);
            });
    }
    

    Notice that the return type of this method is no longer void, but Property; the [Property] attribute understands this type and executes the test accordingly.

    This third option is my preferred way of using custom Arbitraries from within xUnit.net, because it brings back compile-time checking.