Тестирование класса, не объявленного в разделе интерфейса

Я новичок с контейнерами Dependency Injection, и я пытаюсь разобраться, используя их в сочетании с Mocking.

Допустим, у меня есть контроллер и список (модель):

IBlahList = interface
  property Items[AIndex: integer]: IBlah read GetItem;
end;

IController = interface
  property List: IBlahList read GetList;
end;

Реализация IController будет выглядеть примерно так (обратите внимание, это в implementaion раздел:

implementation

TController = class (TInterfacedObject, IController)
private
  FList: IBlahList;
  function GetList: IBlahList;

public
  constructor Create(const AList: IBlahList);

end;

И тогда, конечно, я бы зарегистрировал этот класс (а также один для IBlahList) с GlobalContainer:

GlobalContainer.RegisterType<TController>.Implements<IController>;

Я помещаю TController в implementation section, как предлагают различные источники (ну, в любом случае, Ник Ходжес!), так что мы не можем напрямую ссылаться на класс TController.

Теперь просто скажите, что я хочу проверить мою реализацию ICollection в модульном тесте:

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;

Итак, мой вопрос, должен ли я переместить класс TController в мой interface раздел, чтобы я мог проверить его, или есть какой-то другой способ передать фиктивный IBlahList контроллеру, который мне еще предстоит найти?

3 ответа

Решение

Если у вас есть конкретный класс в разделе реализации, то вы можете представить функцию фабрики (то есть иметь ее в разделе интерфейса), которая создает IController с необходимыми параметрами.

Нет абсолютно никакого смысла иметь реализацию, которую нельзя создать, IMO.

interface

...

function CreateController(AList: IBlahList): IController;

implementation

function CreateController(AList: IBlahList): IController;
begin
  Result := TController.Create(AList);
end;

Ну, вы, вероятно, должны также использовать макет фреймворка в своих тестовых проектах, но в этих случаях я обычно "обманываю" и перемещаю implementation куда мне это нужно, используя условную переменную DUNIT:

// 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}

Затем убедитесь, что любые тестовые проекты определяют условную переменную DUNIT, и переместите все единицы, необходимые для объявления TClassUnderTest, в раздел интерфейса. Последнее вы можете сделать постоянно или под контролем условного DUNIT.

Я могу просто сказать: не слушай Ника в этом случае.

Помещение класса в часть реализации модуля имеет свои недостатки, и вы столкнулись с одним из них.

Весь смысл использования внедрения зависимостей состоит в том, чтобы отделить части вашего кода.

Теперь вы удалили статическую зависимость TController и некоторый класс, который реализует IBlahList, но добавили другую (и намного хуже imo) зависимость: зависимость от контейнера DI.

Не помещайте класс в часть реализации модуля просто для того, чтобы кто-то не мог напрямую создать его в вашем производственном коде. Также не вставляйте зависимость от контейнера DI в этот блок.

Гораздо лучший подход - иметь 3 единицы: интерфейс, класс, регистрация.

Изменить: я предлагаю прочитать эту статью и обратить внимание на подчеркнутые части: http://www.loosecouplings.com/2011/01/dependency-injection-using-di-container.html

Edit2 - добавил псевдокод, чтобы показать, что я имею в виду. Код модульного теста может быть в точности как в вопросе.

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.
Другие вопросы по тегам