Frage

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?

War es hilfreich?

Lösung

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;

Andere Tipps

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.
Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top