Roland Bengtsson
Roland Bengtsson

Reputation: 5158

How can I mock Date and Now?

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

Answers (3)

Stefan Glienke
Stefan Glienke

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

serge
serge

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

Randy Sill
Randy Sill

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

Related Questions