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));
}
}
}
}