Тестирование класса, не объявленного в разделе интерфейса
Я новичок с контейнерами 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.