英文:
Guidance on use of DUnitX in Delphi
问题
I understand that you want a translation of your provided text, excluding the code part. Here's the translated text:
"For too many years I have been writing tightly-coupled spaghetti Delphi code.
I have decided to stop doing this and write only clean testable code from now on.
So, I have been studying clean code, dependency injection, refactoring, decoupling, unit testing, etc, etc (Nick Hodges, Ray Konopka, Uncle Bob, Alister Christie to name a few).
I'm stuck on how to go about unit testing one of my classes (using DUnitX). All the examples I have researched so far show the CUT's dependency interface being mocked and then being injected in the CUT's constructor, like this (from Nick Hodges):
That's fine and I understand all of it.
Unfortunately, my class does not have a constructor. It's only dependency (which is a record, not an interface) is passed in a class procedure, as follows:
This unit contains the interface implemented by my class...
This unit contains my class (that implements the above interface)...
And here is how you might deploy my class...
I created a DUnitX project and started to proceed like Nick's credit card manager example. But this is where I think I have failed to fully grasp the techniques used in unit testing. My Test project has this unit...
How should I test that DoSomething behaves correctly when called?
Is it possible to test MyClass as I have written it or must I restructure it?
Can someone point me in the right direction?"
英文:
For too many years I have been writing tightly-coupled spaghetti Delphi code.
I have decided to stop doing this and write only clean testable code from now on.
So, I have been studying clean code, dependency injection, refactoring, decoupling, unit testing, etc, etc (Nick Hodges, Ray Konopka, Uncle Bob, Alister Christie to name a few).
I'm stuck on how to go about unit testing one of my classes (using DUnitX). All the examples I have researched so far show the CUT's dependency interface being mocked and then being injected in the CUT's constructor, like this (from 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;
That's fine and I understand all of it.
Unfortunately, my class does not have a constructor. It's only dependency (which is a record, not an interface) is passed in a class procedure, as follows:
This unit contains the interface implemented by my class...
unit Unit15;
interface
type
TMyRecord = record
Flag: boolean
end;
IMyInterface = interface(IInvokable)
['{17783F54-0F63-413B-8198-88705CBA318F}']
procedure DoSomething;
end;
implementation
end.
This unit contains my class (that implements the above interface)...
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.
And here is how you might deploy my class...
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.
I created a DUnitX project and started to proceed like Nick's credit card manager example. But this is where I think I have failed to fully grasp the techniques used in unit testing. My Test project has this unit...
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.
How should I test that DoSomething behaves correctly when called?
Is it possible to test MyClass as I have written it or must I restructure it?
Can someone point me in the right direction?
答案1
得分: 3
你只能测试具有某些外部世界和测试框架可访问的副作用(状态更改)的方法和功能。如果您无法观察要测试的方法的效果,那么就无法测试它。
在这种特定情况下,能够测试的能力意味着读取控制台输出缓冲区并检查是否获得了预期的输出。据我所知,有一个Windows API允许您这样做,但我无法说使用该API实施测试会有多容易或多困难。显然,在您的代码中,将写入控制台仅用作示例,但它展示了原则。
那个控制台实际上是您不应该在可测试代码中拥有的硬编码依赖项。这是一种使检查和验证正确性变得困难的依赖性,应尽量避免使用,即使您有某种手段来验证副作用。
在您的情况下,这意味着将TConsoleWriter
引入TMyClass
作为依赖项。然后,该依赖项将作为参数传递或通过依赖注入注入。一旦您拥有了它,您就可以用某种测试替身(模拟)替换该依赖项,然后您可以观察在运行测试时发生的副作用,并验证是否获得了预期的结果。
如果您的代码的副作用与外部依赖无关,而是封装在类本身内部,那么您需要允许访问(只读即可)允许您验证方法调用结果的封装数据。
英文:
You can only test methods and functionality that has some side-effects (state changes) accessible to the outside world and test framework. If you cannot observe the effects of the method you want to test, then you cannot test it.
In this particular case ability to test would mean reading the console output buffer and checking whether you got the expected output written. As far as I know there is a Windows API that allows you to do this, but I cannot say how easy or hard would be to implement test using that API. Obviously, writing to console is used just as example in your code, but it shows the principle.
That console is actually a hard coded dependency you shouldn't have in testable code. It is a dependency that makes it hard to check and verify for correctness and such dependencies should be avoided as much as possible, even if you have some means to verify the side-effects.
In your case that would mean introducing TConsoleWriter
as dependency for TMyClass
. That dependency would be then passed as a parameter of injected through dependency injection. Once you have that, then you can replace that dependency with some test double (mock) and you can observe the side-effects that happen when you run your test and verify whether you got the expected results.
If the side-effects of your code are not related to outside dependency, but are encapsulated within the class itself, then you need to allow access (read-only will suffice) to encapsulated data that allows you to verify the results of a method call.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论