多年来,我一直在编写紧密耦合的 Delphi 代码。我决定停止这样做,从现在开始只编写干净的可测试代码。因此,我一直在研究干净代码、依赖注入、重构、解耦、单元测试等(Nick Hodges、Ray Konopka、Uncle Bob、Alister Christie等)。
我被困在如何对我的一个类进行单元测试(使用DUnitX)上,目前为止我研究的所有例子都显示CUT的依赖接口被模拟,然后被注入到CUT的构造函数中,如下所示(来自Nick Hodges):
procedure TestTCCValidator.TestCardChargeReturnsProperAmountWhenCardIsGood;
var
CCManager: TCreditCardManager;
CCValidator: TMock<ICreditCardValidator>;
GoodCard: String;
Input: Double;
Expected, Actual: Double;
begin
//Arrange
GoodCard := '123456';
Input := 49.95;
Expected := Input;
CCValidator := TMock<ICreditCardValidator>.Create;
CCValidator.Setup.WillReturn(True).When.IsCreditCardValid(GoodCard);
CCManager := TCreditCardManager.Create(CCValidator);
try
//Act
Actual := CCManager.ProcessCreditCard(GoodCard, Input)
finally
CCManager.Free;
end;
// Assert
Assert.AreEqual(Expected, Actual);
end;
很好,我都理解。
不幸的是,我的类没有构造函数,它只有依赖项(是一个记录,而不是接口)在类过程中传递,如下所示:
这个单元包含了我的类实现的接口...
unit Unit15;
interface
type
TMyRecord = record
Flag: boolean
end;
IMyInterface = interface(IInvokable)
['{17783F54-0F63-413B-8198-88705CBA318F}']
procedure DoSomething;
end;
implementation
end.
这个单元包含我的类(实现了上面的接口)...
unit Unit14;
interface
uses
Unit15;
type
TMyClass = class(TInterfacedObject, IMyInterface)
FRec: TMyRecord;
procedure DoSomething;
procedure DoThis;
procedure DoThat;
class procedure Execute(ARec: TMyRecord);
end;
implementation
{ MyClass }
procedure TMyClass.DoSomething;
begin
case FRec.Flag of
true: DoThis;
false: DoThat;
end;
end;
procedure TMyClass.DoThat;
begin
writeln('did that');
end;
procedure TMyClass.DoThis;
begin
writeln('did this');
end;
class procedure TMyClass.Execute(ARec: TMyRecord);
begin
var mc: IMyInterface := TMyClass.Create;
TMyClass(mc).FRec := ARec;
mc.DoSomething;
end;
end.
你可以这样部署我的类...
program Project12;
{$APPTYPE CONSOLE}
{$R *.res}
uses
System.SysUtils,
Unit14 in 'Unit14.pas',
Unit15 in 'Unit15.pas';
var
Rec: TMyRecord;
begin
try
Rec.Flag := true;
TMyClass.Execute(Rec);
ReadLn;
except
on E: Exception do
Writeln(E.ClassName, ': ', E.Message);
end;
end.
我创建了一个DUnitX项目,并开始像Nick的信用卡管理器示例那样进行。但这就是我认为我未能完全掌握单元测试中使用的技术的地方。我的测试项目有这个单元...
unit Unit16;
interface
uses
Unit14,
Unit15,
DUnitX.TestFramework;
type
[TestFixture]
TMyTestObject = class
private
FRec: TMyRecord;
CUT: TMyClass; // do I need this?
public
[Setup]
procedure Setup;
[TearDown]
procedure TearDown;
[Test]
// [TestCase('Text Execute with true flag', true)] // causes compiler error
// [TestCase('Text Execute with false flag', false)] // causes compiler error
procedure TestExecute(const AFlag: boolean);
end;
// how do I mock IMyInterface (which is created in TMyClass.Execute)?
implementation
procedure TMyTestObject.Setup;
begin
end;
procedure TMyTestObject.TearDown;
begin
end;
procedure TMyTestObject.TestExecute(const AFlag: boolean);
begin
FRec.Flag := AFlag;
TMyClass.Execute(FRec);
end;
initialization
TDUnitX.RegisterTestFixture(TMyTestObject);
end.
我应该如何测试DoSomething在被调用时的行为是否正确?
可以按照我编写的那样测试MyClass吗?还是必须重新构造它?
有人能给我指一下正确的方向吗?
1条答案
按热度按时间mutmk8jj1#
只能测试具有某些外部世界和测试框架可访问的副作用(状态更改)的方法和功能。如果无法观察要测试的方法的效果,则无法测试该方法。
在这种特殊情况下,测试能力意味着阅读控制台输出缓冲区,并检查是否写入了预期的输出。据我所知,有一个Windows API允许您这样做,但我不能说使用该API实现测试有多容易或困难。显然,写入控制台只是作为代码中的示例使用,但它显示了原理。
这个控制台实际上是一个硬编码的依赖项,你不应该在可测试代码中使用它,它是一个很难检查和验证正确性的依赖项,应该尽可能避免这样的依赖项,即使你有一些方法来验证副作用。
在您的情况下,这意味着将
TConsoleWriter
作为TMyClass
的依赖项引入。然后,该依赖项将作为通过依赖项注入注入的参数传递。一旦您有了该依赖项,就可以用一些测试双精度浮点数(模拟)替换该依赖项,并且可以观察运行测试时发生的副作用,并验证是否得到了预期的结果。如果代码的副作用与外部依赖项无关,而是封装在类本身中,则需要允许访问(只读就足够了)封装的数据,以便验证方法调用的结果。