Как издеваться над классом, за создание которого отвечает другой класс? - PullRequest
4 голосов
/ 02 мая 2011

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

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, чтобы упростить модульное тестирование.

1 Ответ

8 голосов
/ 02 мая 2011

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

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

Чтобы уменьшить изменения в остальной части вашего кода, вы можете сделать фабрикупараметр имеет значение по умолчанию в конструкторе 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 уже виртуален, так что это еще один способ заставить потомка выполнять дополнительные операции без необходимости виртуального конструктора.)

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...