Delphi-Mocks: макетирование класса с параметрами в конструкторе

Я начинаю использовать платформу Delphi-Mocks, и у меня возникают проблемы с имитацией класса, параметры которого есть в конструкторе. Функция класса "Создать" для TMock не допускает параметры. Если попытаться создать фиктивный экземпляр TFoo.Create( Bar: someType); Я получаю несоответствие количества параметров 'когда TObjectProxy.Create; пытается вызвать метод "Создать" Т.

Очевидно, это связано с тем, что следующий код не передает никакие параметры методу Invoke:

instance := ctor.Invoke(rType.AsInstance.MetaclassType, []);

Я создал перегруженную функцию класса, которая передает параметры:

class function Create( Args: array of TValue ): TMock<T>; overload;static;

и работает с ограниченным тестированием, которое я сделал.

Мой вопрос:

Это ошибка или я просто делаю это неправильно?

Спасибо

PS: я знаю, что Delphi-Mocks ориентирован на интерфейс, но он поддерживает классы, и кодовая база, над которой я работаю, - это 99% Classes.

3 ответа

Решение

Основная проблема, на мой взгляд, заключается в том, что TMock<T>.Create результаты в тестируемом классе (CUT). Я подозреваю, что фреймворк был разработан в предположении, что вы будете издеваться над абстрактным базовым классом. В этом случае создание экземпляра было бы мягким. Я подозреваю, что вы имеете дело с устаревшим кодом, у которого нет удобного абстрактного базового класса для CUT. Но в вашем случае единственный способ создания экземпляра CUT включает в себя передачу параметров конструктору и, таким образом, сводит на нет всю цель насмешки. И я скорее представляю, что будет много работы по реорганизации унаследованной кодовой базы, пока у вас не будет абстрактного базового класса для всех классов, которые необходимо смоделировать.

Вы пишете TMock<TFoo>.Create где TFoo это класс. Это приводит к созданию прокси-объекта. Что происходит в TObjectProxy<T>.Create, Код которого выглядит следующим образом:

constructor TObjectProxy<T>.Create;
var
  ctx   : TRttiContext;
  rType : TRttiType;
  ctor : TRttiMethod;
  instance : TValue;
begin
  inherited;
  ctx := TRttiContext.Create;
  rType := ctx.GetType(TypeInfo(T));
  if rType = nil then
    raise EMockNoRTTIException.Create('No TypeInfo found for T');

  ctor := rType.GetMethod('Create');
  if ctor = nil then
    raise EMockException.Create('Could not find constructor Create on type ' + rType.Name);
  instance := ctor.Invoke(rType.AsInstance.MetaclassType, []);
  FInstance := instance.AsType<T>();
  FVMInterceptor := TVirtualMethodInterceptor.Create(rType.AsInstance.MetaclassType);
  FVMInterceptor.Proxify(instance.AsObject);
  FVMInterceptor.OnBefore := DoBefore;
end;

Как видите, код предполагает, что в вашем классе нет конструктора параметров. Когда вы вызываете это в своем классе, конструктор которого имеет параметры, это приводит к исключению RTTI во время выполнения.

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

Поэтому вместо того, чтобы этот код вызывал конструктор, я предлагаю вам изменить его так, чтобы он вызывал NewInstance, Это необходимый минимум для того, чтобы иметь виртуальную таблицу, которой можно манипулировать. И вам также нужно будет изменить код, чтобы он не пытался уничтожить макет экземпляра и вместо этого вызывал FreeInstance, Все это будет хорошо работать, пока все, что вы делаете, это вызываете виртуальные методы на макете.

Модификации выглядят так:

constructor TObjectProxy<T>.Create;
var
  ctx   : TRttiContext;
  rType : TRttiType;
  NewInstance : TRttiMethod;
  instance : TValue;
begin
  inherited;
  ctx := TRttiContext.Create;
  rType := ctx.GetType(TypeInfo(T));
  if rType = nil then
    raise EMockNoRTTIException.Create('No TypeInfo found for T');

  NewInstance := rType.GetMethod('NewInstance');
  if NewInstance = nil then
    raise EMockException.Create('Could not find NewInstance method on type ' + rType.Name);
  instance := NewInstance.Invoke(rType.AsInstance.MetaclassType, []);
  FInstance := instance.AsType<T>();
  FVMInterceptor := TVirtualMethodInterceptor.Create(rType.AsInstance.MetaclassType);
  FVMInterceptor.Proxify(instance.AsObject);
  FVMInterceptor.OnBefore := DoBefore;
end;

destructor TObjectProxy<T>.Destroy;
begin
  TObject(Pointer(@FInstance)^).FreeInstance;//always dispose of the instance before the interceptor.
  FVMInterceptor.Free;
  inherited;
end;

Честно говоря, это выглядит немного более разумным для меня. Нет смысла вызывать конструкторы и деструкторы.

Пожалуйста, дайте мне знать, если я далеко от цели и упустил суть. Это вполне возможно!

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

type
  TMyClass = class
  public
    constructor Create(AValue: Integer);
  end;

вы можете наследовать этот класс с помощью конструктора без параметров и свойства класса, которое содержит параметр

type
  TMyClassMockable = class(TMyClass)
  private
  class var
    FACreateParam: Integer;
  public
    constructor Create;
    class property ACreateParam: Integer read FACreateParam write FACreateParam;
  end;

constructor TMyClassMockable.Create;
begin
  inherited Create(ACreateParam);
end;

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

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

Само собой разумеется, что этот подход не является потокобезопасным.

Отказ от ответственности: я ничего не знаю о Delphi-Mocks.

Я предполагаю, что это по замыслу. Из вашего примера кода похоже, что Delphi-Mocks использует дженерики. Если вы хотите создать экземпляр экземпляра универсального параметра, как в:

function TSomeClass<T>.CreateType: T;
begin
  Result := T.Create;
end;

тогда вам нужно ограничение конструктора на общий класс:

TSomeClass<T: class, constructor> = class

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

Вы могли бы сделать что-то вроде

TSomeClass<T: TSomeBaseMockableClass, constructor> = class

и дать TSomeBaseMockableClass конкретный конструктор, который затем может быть использован, НО:

Требовать, чтобы все пользователи вашей платформы извлекали все свои классы из определенного базового класса, просто... ну... чрезмерно ограничительно (мягко говоря) и особенно с учетом единственного наследования Delphi.

Другие вопросы по тегам