Some time ago I had to modify a code sequence that populates newly created items.

In time, the rules to generate a newly created task start date evolved to the following set:

  • when no tasks exist, the start date is today
  • when tasks exist
    • if the option “With time data” is toggled, the start date is the maximum known date
    • if the option “With time data” is untoggled, the start date is the maximum known date advanced to the next working day
  • when license is free, the time value is 12:00 AM
  • when license is Premium, the time value is the configured workday start

As a result, the code evolved to the following form:


var defaultStartTime = Document.TimeConfiguration.WorkDayStart;

var startDate = Document.Tasks.Select( i => i.StartDate).DefaultIfEmpty(DateTime.Today).Max();

if( Document.Tasks.Count > 0 )
{
if( Document.Version.IsPlusEdition )
{
if( !Document.TimeConfiguration.UseTime )
{
startDate = startDate.AddDays( 1 );
}
}
else
{
startDate = startDate.AddDays( 1 );
}
}

startDate = WorkingDaysManager.ToWorkingDay( startDate, Document.Scale.WorkingDays );
startDate = startDate.SetTimeOfDay( defaultStartTime );

Not a nice sequence …

I’ve decided to see for myself if I can rewrite this sequence using TDD.

The results are promising (I can’t publish the code in WordPress, I’ll have to use some other system). I’ll get back to this experiment as soon as time allows, to settle on an unit tests naming convention (as you can see, the tests have awfully long names).


public DateTime Get()
{
var maxKnownDate = _existingDates.DefaultIfEmpty( _today ).Max();
DateTime result;
if( _timeConfiguration.UseTime )
{
result = maxKnownDate;
}
else
{
result = _existingDates.Count == 0 ?
maxKnownDate :
_workingDaysProcessor.GetNextWorkingDay( maxKnownDate, _workingDaysConfiguration );
}

var timeOfDay = _isPlusEdition ? _timeConfiguration.WorkDayStart : DateTimeExtensions.DayStart;
result = result.SetTimeOfDay( timeOfDay );

return result;
}

And the core tests (the ones that I’ve written first, failed and after updating the code succeeded).


public class NewTaskStartDateCalculatorTests
{
[Theory, AutoData]
public void When_NoTasksExist_StartDatePart_EqualsToday( DateTime today )
{
var builder = new NewTaskStartDateCalculator.Builder();
var calculator = builder
.WithToday( today )
.Build();

var startDate = calculator.Get();

Assert.Equal( today.Date, startDate.Date );
}

[Theory, AutoData]
public void WhenOneTaskExists_And_UseTimeIsOn_StartDatePart_EqualsExistingTaskStartDateDatePart( DateTime task1StartDate )
{
var builder = new NewTaskStartDateCalculator.Builder();
var calculator = builder
.WithKnownTasksStartDates( new List<DateTime> { task1StartDate } )
.WithUseTime()
.Build();

var startDate = calculator.Get();

Assert.Equal( task1StartDate.Date, startDate.Date );
}

public static IEnumerable<object[]> TestData1 => new[]
{
new object[] { new DateTime( 2017, 1, 2 ), WorkingDays.Standard, new DateTime( 2017, 1, 3 ) }
};
[Theory]
[MemberData( nameof( TestData1 ) ) ]
public void WhenOneTaskExists_And_UseTimeIsOff_StartDatePart_IsExistingStartDateAdvancedToTheNextWorkingDayDatePart(
DateTime task1StartDate,
WorkingDays workingDays,
DateTime nextWorkingDay )
{

var fakeWorkingDaysProcessor = A.Fake<IWorkingDaysProcessor>();
A.CallTo(
() => fakeWorkingDaysProcessor.GetNextWorkingDay( task1StartDate, workingDays )
).Returns( nextWorkingDay );

var builder = new NewTaskStartDateCalculator.Builder();
var calculator = builder
.WithKnownTasksStartDates(new List<DateTime> { task1StartDate })
.WithWorkingDaysConfiguration( workingDays )
.WithWorkingDaysProcessor( fakeWorkingDaysProcessor )
.Build();

var startDate = calculator.Get();

Assert.Equal( nextWorkingDay.Date, startDate.Date );
}
[Theory, AutoData ]
public void WhenIsPlusEdition_StartTime_EqualsConfiguredWorkdayStart(
DateTime today, TimeSpan workDayStart )
{
// TODO: the TimeSpan object has milliseconds.
// Implement a mechanism to generate TimeSpan objects without milliseconds.
// In addition, analyze the design to ensure the builder properly handles
// the situation where the input TimeSpan has milliseconds (should it
// silently cut the milliseconds or throw an exception?)
workDayStart = workDayStart.WithZeroMilliseconds();
var builder = new NewTaskStartDateCalculator.Builder();
var calculator = builder
.WithToday( today )
.WithWorkDayStart( workDayStart )
.WithLicenseIsPlusEdition()
.Build();

var startDate = calculator.Get();

Assert.Equal( workDayStart, startDate.TimeOfDay );
}

[Theory, AutoData]
public void WhenIsFreeEdition_StartTime_Equals12_00_AM( DateTime today, DateTime task1StartDate, DateTime fakeNextWorkingDate )
{
var fakeWorkingDaysProcessor = A.Fake<IWorkingDaysProcessor>();
A.CallTo(
() => fakeWorkingDaysProcessor.GetNextWorkingDay( A<DateTime>.Ignored, A<WorkingDays>.Ignored )
).Returns( fakeNextWorkingDate );

var builder = new NewTaskStartDateCalculator.Builder();
var calculator = builder
.WithKnownTasksStartDates( new List<DateTime>{ task1StartDate } )
.WithWorkingDaysProcessor( fakeWorkingDaysProcessor )
.Build();

var startDate = calculator.Get();

Assert.Equal( DateTimeExtensions.DayStart, startDate.TimeOfDay );
}
}

Advertisements