Testing a class not declared in the interface section
-
13-06-2021 - |
题
I'm a newbie with Dependency Injection containers, and I am trying to get my head around using them in conjunction with Mocking.
Lets say I have a controller and a list (the model):
IBlahList = interface
property Items[AIndex: integer]: IBlah read GetItem;
end;
IController = interface
property List: IBlahList read GetList;
end;
The implementation of IController would look something like (note, it's in the implementaion
section:
implementation
TController = class (TInterfacedObject, IController)
private
FList: IBlahList;
function GetList: IBlahList;
public
constructor Create(const AList: IBlahList);
end;
And then, of course, I would register this class (as well as one for IBlahList) with the GlobalContainer
:
GlobalContainer.RegisterType<TController>.Implements<IController>;
I place the TController in the implementation
section, as suggested by various sources (well, Nick Hodges anyway!), so that we cannot reference the TController class directly.
Now, just say I want to test my implementation of ICollection in a unit test:
procedure TestSomething
var
LMockList: TMock<IBlahList>;
LController: IController;
begin
LMockList := TMock<IBlahList>.Create;
// Oops, I can't do this, I can't access TController
LController := TController.Create(LMockList);
end;
So, my question is, should I move the TController class into my interface
section so I can test it, or is there some other way to pass the mock IBlahList to the controller that I have yet to find?
解决方案
If you have the concrete class in the implementation section, then you could expose a factory function (i.e. have it in the interface section) that creates an IController with the required parameters.
It makes absolutely no sense to have an implementation that can not be instantiated, IMO.
interface
...
function CreateController(AList: IBlahList): IController;
implementation
function CreateController(AList: IBlahList): IController;
begin
Result := TController.Create(AList);
end;
其他提示
Well you probably should be using the mock framework in your test projects as well, but in these cases I usually "cheat" and move the implementation
to where I need it using a DUNIT conditional variable:
// In the real app, we want the implementation and uses clauses here.
{$IFNDEF DUNIT}
implementation
uses
classes;
{$ENDIF}
type
TClassUnderTest = class(TObject)
// ...
end;
// In test projects it is more convenient to have the implemenation and
// uses clauses down here.
{$IFDEF DUNIT}
implementation
uses
classes;
{$ENDIF}
Then make sure that any test projects define the DUNIT conditional var, and move any units needed by the TClassUnderTest declaration to the interface section. The latter you can do permanently or under control of the DUNIT conditional as well.
I can just say: don't listen to Nick in that case.
Putting a class inside the implementation part of a unit just has disadvantages and you are facing one of them.
The whole point of using dependency injection is to decouple pieces of your code.
Now you removed the static dependency of TController and some class that implements IBlahList but you pulled in another (and much worse imo) dependency: the dependency on the DI container.
Don't put the class inside the implementation part of a unit just to prevent someone from directly creating it in your production code. Also don't put in the dependency on the DI container into that unit.
A much better approach is to have 3 units: interface, class, registration.
Edit: I suggest reading this article and pay attention to the underlined parts: http://www.loosecouplings.com/2011/01/dependency-injection-using-di-container.html
Edit2 - added some pseudo code to show what I mean. The unit test code could exactly be as in the question.
unit Interfaces;
interface
type
IBlahList = interface
property Items[AIndex: integer]: IBlah read GetItem;
end;
IController = interface
property List: IBlahList read GetList;
end;
implementation
end.
-
unit Controller;
interface
uses
Classes,
Interfaces;
type
TController = class (TInterfacedObject, IController)
private
FList: IBlahList;
function GetList: IBlahList;
public
constructor Create(const AList: IBlahList);
end;
implementation
...
end.
-
unit Registration;
interface
implementation
uses
Interfaces,
Controller,
Spring.Container;
initialization
GlobalContainer.RegisterType<TController>.Implements<IController>;
end.