我的某些代码使用Date and Now。因此,对该代码执行测试的结果取决于执行时间。那当然是不好的。 那么,在测试中建议使用伪造日期和时间的推荐方式是什么?
使用界面吗?
答案 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年的方法。实现方法以使用您自己的方法来替换Date
和Now
函数,这些函数允许覆盖,然后替换代码中的所有引用以利用替换。当您需要测试时,只需将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
函数。
但是,当一个好的程序员使用单元前缀来调用函数时,它通常是不起作用的。