Reputation: 5158
Some of my code use Date and Now. So the result for tests on that code is different depending on when it execute. That is of course bad. So what is the recommended way of fake Date and Time in tests ?
Using interface ?
Upvotes: 1
Views: 468
Reputation: 21713
In order to make things testable you need to have or create seams in your code.
You might want to use the Ambient Context design pattern to achieve this in this particular example. While this looks like a singleton at first it actually allows temporarily replacing the used instance or the returned data. The following is an example how to implement it in Delphi based on this blog post.
I left out the thread safety and all that stuff to give you the basic idea how it works. You might also get rid of TDateTimeProvider singleton/class altogether and just make your own Now
function that works with mocking.
program AmbientContextDemo;
uses
Generics.Collections,
TestFramework,
TestInsight.DUnit,
System.SysUtils;
type
TDateTimeProvider = class
strict private
class var fInstance: TDateTimeProvider;
class function GetInstance: TDateTimeProvider; static;
public
function Now: TDateTime;
class property Instance: TDateTimeProvider read GetInstance;
end;
TDateTimeProviderContext = class
strict private
var fContextNow: TDateTime;
class var contextStack: TStack<TDateTimeProviderContext>;
class function GetCurrent: TDateTimeProviderContext; static;
public
class constructor Create;
class destructor Destroy;
constructor Create(const contextNow: TDateTime);
destructor Destroy; override;
property ContextNow: TDateTime read fContextNow;
class property Current: TDateTimeProviderContext read GetCurrent;
end;
TStuff = class
// class procedure for demo purpose, no instance necessary in test
class function DoSomeDateTimeStuff_UsingNow: TDateTime;
class function DoSomeDateTimeStuff_UsingAmbientContext: TDateTime;
end;
TMyTest = class(TTestCase)
published
procedure TestIt_Fail;
procedure TestIt_Pass;
end;
{ TDateTimeProvider }
class function TDateTimeProvider.GetInstance: TDateTimeProvider;
begin
if not Assigned(fInstance) then
fInstance := TDateTimeProvider.Create;
Result := fInstance;
end;
function TDateTimeProvider.Now: TDateTime;
var
context: TDateTimeProviderContext;
begin
context := TDateTimeProviderContext.Current;
if Assigned(context) then
Result := context.ContextNow
else
Result := System.SysUtils.Now;
end;
{ TMyTest }
procedure TMyTest.TestIt_Fail;
begin
CheckEquals(EncodeDate(2018, 11, 11), TStuff.DoSomeDateTimeStuff_UsingNow);
end;
procedure TMyTest.TestIt_Pass;
var
ctx: TDateTimeProviderContext;
begin
ctx := TDateTimeProviderContext.Create(EncodeDate(2018, 11, 11));
try
CheckEquals(EncodeDate(2018, 11, 11), TStuff.DoSomeDateTimeStuff_UsingAmbientContext);
finally
ctx.Free;
end;
end;
{ TStuff }
class function TStuff.DoSomeDateTimeStuff_UsingNow: TDateTime;
begin
Result := Now; // using Now which is unmockable, only via hooking but that affects all occurences even in the RTL
end;
class function TStuff.DoSomeDateTimeStuff_UsingAmbientContext: TDateTime;
begin
Result := TDateTimeProvider.Instance.Now;
end;
{ TDateTimeProviderContext }
class constructor TDateTimeProviderContext.Create;
begin
contextStack := TStack<TDateTimeProviderContext>.Create;
end;
class destructor TDateTimeProviderContext.Destroy;
begin
contextStack.Free;
end;
constructor TDateTimeProviderContext.Create(const contextNow: TDateTime);
begin
fContextNow := contextNow;
contextStack.Push(Self);
end;
destructor TDateTimeProviderContext.Destroy;
begin
contextStack.Pop;
end;
class function TDateTimeProviderContext.GetCurrent: TDateTimeProviderContext;
begin
if contextStack.Count = 0 then
Result := nil
else
Result := contextStack.Peek;
end;
begin
RegisterTest(TMyTest.Suite);
RunRegisteredTests;
end.
Upvotes: 4
Reputation: 1022
Consider you call Date()
and Now()
without unit prefix like SysUtils.Date()
. Then add MyTestUtils.pas
unit to the end of uses
with conditional compiling
{$IFDEF TestMode}
, MyTestUtils
{$ENDIF}
In MyTestUtils.pas
you can define your own Date()
and Now()
functions that will be used instead of SysUtils
ones.
However it's a "nasty hack" that normally doesn't work when a good programmer uses unit prefixes to call functions.
Upvotes: 1
Reputation: 353
It's mentioned above, but some example code may help. Here's a method I've been using for 20 years. Implement methods to replace Date
and Now
functions with your own that allow an override, then replace all references in your code to utilize the replacements. When you need to test, simply set TestDate to a value appropriate for your tests.
var
TestDate: TDateTime = 0;
function CurrDate: TDateTime;
begin
if TestDate = 0 then
Result := SysUtils.Date
else
Result := TestDate;
end;
function CurrDateTime: TDateTime;
begin
if (TestDate = 0) then
Result := Now
else
Result := TestDate + Time;
end;
Upvotes: 2