开发者

How do I mock a class that another class is responsible for instantiating?

Please consider the following code:

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;

I am creating unit tests for a class and I am stuck on it. My questions are all about the best way to mock objects and the design pattern I should use. The class I am unit testing was not created by me.

  1. In the following example, I need to mock Foo1 because it sends a request to a Web service that I cannot call during my unit testing. But Foo1 is being created by the TFoo2 constructor and there开发者_如何学编程's no way I can mock it. What should I do in this case? Should I modify the TFoo2 constructor to accept the Foo1 object like this?

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

    Is there a design pattern that says we need to pass all objects that a class depends on, like the example above?

  2. The method TFoo2.DoSomething3 creates the Foo1 object and then frees it. Should I also modify that code to pass a Foo1 object?

    procedure TFoo2.DoSomething3(aFoo1 : TFoo1);
    begin
      aFoo1 := aFoo1.DoSomething1;
    end;
    
  3. Is there any design pattern that support the suggestions I made? If so, I could tell all developers in the company I work for that we need to follow the XXX pattern in order to make unit testing easier.


If you cannot mock the creation of TFoo1, then you cannot mock TFoo1. Right now, TFoo2 is responsible for creating all instances of TFoo1, but if that's not the primary purpose of TFoo2, then that is indeed going to make unit-testing hard.

One solution is, as you've suggested, to pass TFoo2 whatever TFoo1 instances it needs. That can complicate all your current code that already calls TFoo2 methods. Another way, which is a little more unit-testing-friendly, is to provide a factory for TFoo1. The factory can be as simple as a function pointer, or it can be a whole class. In Delphi, metaclasses can also serve as factories. Pass the factory to TFoo2 when it's constructed, and whenever TFoo2 needs a TFoo1 instance, it can invoke the factory.

To reduce changes to the rest of your code, you can make the factory parameter have a default value in the TFoo2 constructor. Then you don't have to change your application code. Just change your unit-testing code to provide a non-default factory argument.

Whatever you do, you'll need to make TFoo1.DoSomething1 be virtual, or else mocking will be futile.

Using metaclasses, your code can look like this:

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;

Now, your unit-testing code can provide a mock version of TFoo1 and pass it when it creates a 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;

Many examples of metaclasses give the base class a virtual constructor, but that isn't strictly necessary. You only need to have a virtual constructor if the constructor needs to be called virtually — if the descendant constructor will need to do things with the constructor parameters that the base class doesn't already do. If the descendant (TMockFoo1, in this case) does all the same things as its ancestor, then the constructor doesn't need to be virtual. (Also remember that AfterConstruction is already virtual, so that's another way to get the descendant to do additional operations without the need for a virtual constructor.)

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜