Как мне издеваться над классом, за создание экземпляра которого отвечает другой класс?

Пожалуйста, рассмотрите следующий код:

type
  TFoo1 = class
  public
    procedure DoSomething1;
  end;

  TFoo2 = class
  private
    oFoo1 : TFoo1;
  public
    procedure DoSomething2;
    procedure DoSomething3;
    constructor Create;
    destructor Destroy; override;
  end;


procedure TFoo1.DoSomething1;
begin
  ShowMessage('TFoo1');
end;

constructor TFoo2.Create;
begin
  oFoo1 := TFoo1.Create;
end;

destructor TFoo2.Destroy;
begin
  oFoo1.Free;
  inherited;
end;

procedure TFoo2.DoSomething2;
begin
  oFoo1.DoSomething1;
end;

procedure TFoo2.DoSomething3;
var
  oFoo1 : TFoo1;
begin
  oFoo1 := TFoo1.Create;
  try
    oFoo1.DoSomething1;
  finally
    oFoo1.Free;
  end;
end;

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

  1. В следующем примере мне нужно имитировать Foo1, потому что он отправляет запрос в веб-службу, которую я не могу вызвать во время модульного тестирования. Но Foo1 создается конструктором TFoo2, и я никак не могу над ним издеваться. Что мне делать в этом случае? Должен ли я изменить конструктор TFoo2, чтобы он принимал объект Foo1 таким образом?

    constructor TFoo2.Create(aFoo1 : TFoo1)
    begin
      oFoo1 := aFoo1;
    end;
    

    Существует ли шаблон проектирования, который говорит, что нам нужно передавать все объекты, от которых зависит класс, как в примере выше?

  2. Метод TFoo2.DoSomething3 создает объект Foo1, а затем освобождает его. Должен ли я также изменить этот код для передачи объекта Foo1?

    procedure TFoo2.DoSomething3(aFoo1 : TFoo1);
    begin
      aFoo1 := aFoo1.DoSomething1;
    end;
    
  3. Есть ли какой-либо шаблон проектирования, который поддерживает мои предложения? Если это так, я мог бы сказать всем разработчикам в компании, в которой я работаю, что нам нужно следовать шаблону XXX, чтобы упростить модульное тестирование.


person Rafael Colucci    schedule 02.05.2011    source источник


Ответы (1)


Если вы не можете издеваться над созданием TFoo1, то вы не можете издеваться над TFoo1. Прямо сейчас TFoo2 отвечает за создание всех экземпляров TFoo1, но если это не является основной целью TFoo2, то это действительно затруднит модульное тестирование.

Одним из решений, как вы предложили, является передача TFoo2 любых TFoo1 экземпляров, которые ему нужны. Это может усложнить весь ваш текущий код, который уже вызывает TFoo2 методов. Другой способ, более удобный для модульного тестирования, — предоставить фабрику для TFoo1. Фабрика может быть как просто указателем на функцию, так и целым классом. В Delphi метаклассы также могут служить фабриками. Передайте фабрику TFoo2, когда она будет построена, и всякий раз, когда TFoo2 нужен экземпляр TFoo1, она может вызвать фабрику.

Чтобы уменьшить количество изменений в остальной части вашего кода, вы можете задать для параметра factory значение по умолчанию в конструкторе TFoo2. Тогда вам не нужно менять код приложения. Просто измените свой код модульного тестирования, чтобы предоставить фабричный аргумент, отличный от значения по умолчанию.

Что бы вы ни делали, вам нужно сделать TFoo1.DoSomething1 виртуальным, иначе насмешки будут бесполезны.

Используя метаклассы, ваш код может выглядеть так:

type
  TFoo1 = class
    procedure DoSomethign1; virtual;
  end;

  TFoo1Class = class of TFoo1;

  TFoo2 = class
  private
    oFoo1 : TFoo1;
    FFoo1Factory: TFoo1Class;
  public
    constructor Create(AFoo1Factory: TFoo1Class = nil);
  end;

constructor TFoo2.Create;
begin
  inherited Create;
  FFoo1Factory := AFoo1Factory;
  if not Assigned(FFoo1Factory) then
    FFoo1Factory := TFoo1;

  oFoo1 := FFoo1Factory.Create;
end;

Теперь ваш код модульного тестирования может предоставить фиктивную версию TFoo1 и передать ее при создании TFoo2:

type
  TMockFoo1 = class(TFoo1)
    procedure DoSomething1; override;
  end;

procedure TMockFoo1.DoSomething1;
begin
  // TODO: Pretend to access Web service
end;

procedure TestFoo2;
var
  Foo2: TFoo2;
begin
  Foo2 := TFoo2.Create(TMockFoo1);
end;

Многие примеры метаклассов предоставляют базовому классу виртуальный конструктор, но это не обязательно. Вам нужно иметь виртуальный конструктор только в том случае, если конструктор нужно вызывать виртуально — если конструктору-потомку нужно будет делать что-то с параметрами конструктора, чего базовый класс еще не делает. Если потомок (в данном случае TMockFoo1) делает все то же самое, что и его предок, то конструктор не обязательно должен быть виртуальным. (Также помните, что AfterConstruction уже является виртуальным, так что это еще один способ заставить потомка выполнять дополнительные операции без необходимости в виртуальном конструкторе.)

person Rob Kennedy    schedule 02.05.2011
comment
Знаете ли вы, есть ли какой-либо шаблон проектирования, который говорит нам, что нам нужно передавать зависимости объектов через конструктор или что-то еще? Это шаблон декоратора? - person Rafael Colucci; 02.05.2011
comment
То, что вы ищете, это внедрение зависимостей или инверсия управления. Вы создали зависимость TFoo1 от TFoo2. Вместо этого вы хотите внедрить TFoo2 в TFoo1 через интерфейс или конструктор. Помните, если вам трудно писать модульные тесты, значит, вы делаете это неправильно. ;-) - person Nick Hodges; 02.05.2011
comment
+1: Я также предлагаю посмотреть некоторые статьи Миско Хевери и видео на You Tube. Он даст вам несколько отличных идей, как сделать ваш код более тестируемым. Например: Clean Code Talks Dependency Injection - person Disillusioned; 02.05.2011
comment
@Крейг, очень хороший совет. Спасибо - person Rafael Colucci; 02.05.2011
comment
@Ник. Да, это было именно то имя, которое я искал. - person Rafael Colucci; 02.05.2011