如何模拟日期和现在?

时间:2018-11-12 11:28:15

标签: delphi

我的某些代码使用Date and Now。因此,对该代码执行测试的结果取决于执行时间。那当然是不好的。 那么,在测试中建议使用伪造日期和时间的推荐方式是什么?

使用界面吗?

3 个答案:

答案 0 :(得分:4)

为了使事物可测试,您需要在代码中具有或创建seams

在此特定示例中,您可能希望使用环境上下文设计模式来实现此目的。虽然一开始看起来像单例,但实际上允许临时替换使用的实例或返回的数据。以下是一个示例,说明如何基于this blog post在Delphi中实现它。

我省略了线程安全性和所有其他内容,以向您提供其工作原理的基本概念。您可能还完全摆脱了TDateTimeProvider单例/类,而只是制作了自己的Now函数,可以与模拟一起使用。

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.

答案 1 :(得分:2)

上面已经提到,但是一些示例代码可能会有所帮助。这是我使用了20年的方法。实现方法以使用您自己的方法来替换DateNow函数,这些函数允许覆盖,然后替换代码中的所有引用以利用替换。当您需要测试时,只需将TestDate设置为适合您的测试的值。

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;

答案 2 :(得分:1)

请考虑您呼叫Date()Now()而不带单位前缀,例如SysUtils.Date()。然后通过条件编译将MyTestUtils.pas单元添加到uses的末尾

{$IFDEF TestMode}
, MyTestUtils
{$ENDIF}

MyTestUtils.pas中,您可以定义自己的Date()Now()函数,而不是SysUtils函数。

但是,当一个好的程序员使用单元前缀来调用函数时,它通常是不起作用的。