简体   繁体   中英

How to add explicit edge cases to a generator / arbitrary in FsCheck w/ C#?

Background

I have two extension methods, DateOnly.BeginningOfDay & DateOnly.EndOfDay , that are supposed to help in turning a DateOnly object into a DateTime object. The tests for these methods are written in FsCheck. FsCheck has no built in generators for the DateOnly and TimeOnly types so I have made my own generators for these tests. After checking my code into our CI pipeline I eventually ran into an issue for DateOnly.BeginningOfDay where the tests didn't take into account the edge case of the TimeOnly value being generated as 12:00:00 AM, the equivalent of TimeOnly.MinValue . I adjusted the tests and re-ran the CI pipeline and ham-fisted these edge cases to be run more frequently using a Gen.OneOf combo of some constant gens with the original TimeOnly gen. I know FsCheck does some special edge case checking for its default values. I was wondering if there was a way to define a set of edge cases for a type that would be automatically generated during each run of the test without treating them as a Gen.OneOf option (after all, we only want some of these edge cases run once, any more is a waste of time and power).

The Code

DateOnlyExtensions.cs

public static class DateOnlyExtensions 
{
  public static DateTime BeginningOfDay(this DateOnly date) =>
      date.ToDateTime(TimeOnly.MinValue);

  public static DateTime EndOfDay(this DateOnly date) =>
      date.ToDateTime(TimeOnly.MaxValue);
}

DateOnlyExtensionTests.cs

Note

The only reason that the arguments are being grouped into a singular argument generator was because my project was having trouble recognizing the typing for something like Prop.ForAll(OneArb, TwoArb, RedArb, BlueArb, (one, two, red, blue) =>...) .

Before adding explicit edge cases

public class DateOnlyExtensionTests
    {
        static Gen<int> MakeIntInRangeGen(int start, int end) =>
            from @int in Arb.Generate<int>()
            where @int >= start && @int < end
            select @int;

        static Gen<DateOnly> DateOnlyGen =>
            from dateTime in Arb.Generate<DateTime>()
            select DateOnly.FromDateTime(dateTime);

        static Gen<TimeOnly> TimeOnlyGen =>
            from hour in MakeIntInRangeGen(0, 24)
            from minute in MakeIntInRangeGen(0, 60)
            from second in MakeIntInRangeGen(0, 60)
            from millisecond in MakeIntInRangeGen(0, 1000)
            select new TimeOnly(hour, minute, second, millisecond);

        public class BeginningOfDay
        {
            [Property]
            public Property ShouldReturnTheEarliestTimeInTheDay()
            {
                var argsGen =
                    from time in TimeOnlyGen
                    from date in DateOnlyGen
                    select new { date, time };

                return Prop.ForAll(
                    argsGen.ToArbitrary(),
                    args => args.date.BeginningOfDay() <= args.date.ToDateTime(args.time));
            }
        }

        public class EndOfDay
        {
            [Property]
            public Property ShouldReturnTheLatestTimeInTheDay()
            {
                var argsGen =
                    from time in TimeOnlyGen
                    from date in DateOnlyGen
                    select new { date, time };

                return Prop.ForAll(
                    argsGen.ToArbitrary(),
                    args => args.date.EndOfDay() >= args.date.ToDateTime(args.time));
            }
        }
    }
}

After adding explicit edge cases

using System;
using Core.Dates;
using FluentAssertions;
using FsCheck;
using FsCheck.Xunit;
using TestUtilities;
using static Core.Math.MathHelpers;

namespace Core.Tests.Dates
{
    public class DateOnlyExtensionTests
    {
        static Gen<int> MakeIntInRangeGen(int start, int end) =>
            from @int in Arb.Generate<int>()
            where @int >= start && @int < end
            select @int;

        static Gen<DateOnly> DateOnlyGen =>
            from dateTime in Arb.Generate<DateTime>()
            select DateOnly.FromDateTime(dateTime);

        static Gen<TimeOnly> TimeOnlyGen =>
            Gen.OneOf(TimeOnlyEdgeCaseGen, TimeOnlyRandomGen);

        static Gen<TimeOnly> TimeOnlyEdgeCaseGen =>
            Gen.OneOf(
                Gen.Constant(TimeOnly.MinValue),
                Gen.Constant(TimeOnly.MaxValue));

        static Gen<TimeOnly> TimeOnlyRandomGen =>
            from hour in MakeIntInRangeGen(0, 24)
            from minute in MakeIntInRangeGen(0, 60)
            from second in MakeIntInRangeGen(0, 60)
            from millisecond in MakeIntInRangeGen(0, 1000)
            select new TimeOnly(hour, minute, second, millisecond);

        public class BeginningOfDay
        {
            [Property]
            public Property ShouldReturnTheEarliestTimeInTheDay()
            {
                var argsGen =
                    from time in TimeOnlyGen
                    from date in DateOnlyGen
                    select new { date, time };

                return Prop.ForAll(
                    argsGen.ToArbitrary(),
                    args => args.date.BeginningOfDay() <= args.date.ToDateTime(args.time));
            }
        }

        public class EndOfDay
        {
            [Property]
            public Property ShouldReturnTheLatestTimeInTheDay()
            {
                var argsGen =
                    from time in TimeOnlyGen
                    from date in DateOnlyGen
                    select new { date, time };

                return Prop.ForAll(
                    argsGen.ToArbitrary(),
                    args => args.date.EndOfDay() >= args.date.ToDateTime(args.time));
            }
        }
    }
}

Thanks to @MarkSeemann for the answer. I will document the response here for the sake of completing the question.

Due to the way that FsCheck generates test data there is no guaranteed way of ensuring that specific data is given to validate the property. In this case the best way to accomplish what I was looking for is to have two tests, one for verifying the property with FsCheck, and the other to verify the property using basic xUnit for specific edge cases.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM