2012-06-09 1 views
3

Я новичок с контейнерами для инъекций зависимостей, и я пытаюсь использовать их в сочетании с 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, как это было предложено различными источниками (ну, Ник Ходжес в любом случае!), Так что мы не можем напрямую ссылаться на класс 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 контроллеру, который я еще не нашел?

ответ

4

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

Не имеет смысла иметь реализацию, которая не может быть создана, ИМО.

interface 

... 

function CreateController(AList: IBlahList): IController; 

implementation 

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

+1 Это хорошее решение. Я могу видеть только один недостаток: код приложения (в отличие от кода тестирования) теперь может также напрямую создавать контроллер вместо того, чтобы _forced_ проходить через GlobalContainer (макет-фреймворк). Но вы можете, конечно, поместить объявление функции в раздел интерфейса под управлением условного обозначения DUNIT. Что выглядит намного чище, чем перемещение предложения 'реализация'. –

+1

Подождите секунду. Вы хотите, чтобы код приложения проходил через фреймворк? Зачем ты этого хочешь? –

2

Ну вы, вероятно, должно использовать фиктивные рамки в тестовых проектах, а также, но в этих случаях я обычно «обмануть» и переместить 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} 

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

+0

Если вы должны изменить свои источники таким образом, как это для своих модульных тестов, которые вы делаете что-то неправильно (в этом случае он ставит класс внутри части реализации единицы) –

+0

Я не согласен. Код приложения и тестовый код имеют свои собственные требования. В коде приложения вы хотите убедиться, что создание класса напрямую просто невозможно. В тестовом коде создание класса напрямую - это именно то, что вы хотите сделать. Использование условных определений так же хорошо, как и любой другой, для удовлетворения обоих требований. Решение Руди чище, но все же позволяет прямое создание класса. Объединение этих двух объектов путем размещения объявления функции в разделе интерфейса под управлением условного var является самым чистым способом удовлетворения обоих требований. –

+0

Нет, они оба просто скрываются/пытаются решить проблему, о которой я указал в своем ответе. –

2

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

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

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

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

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

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

Edit: Я предлагаю прочитать эту статью и обратить внимание на подчеркнутые части: 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. 
+0

Хотя это хорошая общая привлекательность, и ссылка интересна, она не показывает, как ее реализовать, и как избежать затягивания зависимости от контейнера DI. Есть ли вероятность того, что это может быть достигнуто в приложении __both__ и тестовом коде? –

+0

@Marjan: ISTM, что многие люди, включая вас, слепо применяют принципы, изложенные в каком-то учебнике, вместо того, чтобы думать о том, как это может вам помочь. Я полностью согласен со Стефаном в том, что реализация класса mock никогда не должна выполняться в той же единице, что и интерфейс. Это обеспечивает больше сцепления, чем вам хотелось бы. Поместите каждого в свою единицу. Класс mock должен знать интерфейс, и реиграция должна знать оба. Но интерфейс не должен сочетаться с классом макета. Стефан показывает вам, как это сделать. –

+0

FWIW, мне действительно интересно, как компиляция IBlahList в исходном примере. Помимо того факта, что мы не знаем IBlah, мы также не видим никаких * методов * и ISTM, которые они должны присутствовать для того, чтобы свойство функционировало. Это показывает мне, что мы не видим реального кода. –

 Смежные вопросы

  • Нет связанных вопросов^_^