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.
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?
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;
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.)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With