Dependency injection
A direct implementation of dependency injection at a class
level can be implemented in Delphi as such:
- All external dependencies shall be defined as abstract
interface
;
- An external factory could be used to retrieve an interface
instance, or class constructor
shall receive the
dependencies as parameters.
Using an external factory can be made within mORMot via
TServiceFactory
- see 63. In the future, we may implement
automated dependency injection.
Here, we will use the more direct constructor
-based pattern for
a simple "forgot my password" scenario.
This is the class we want to test:
TLoginController = class(TInterfacedObject,ILoginController) protected fUserRepository: IUserRepository; fSmsSender: ISmsSender; public constructor Create(const aUserRepository: IUserRepository; const aSmsSender: ISmsSender); procedure ForgotMyPassword(const UserName: RawUTF8); end;
The constructor
will indeed inject its dependencies
into its own instance:
constructor TLoginController.Create(const aUserRepository: IUserRepository; const aSmsSender: ISmsSender); begin fUserRepository := aUserRepository; fSmsSender := aSmsSender; end;
The dependencies are defined with the following two interfaces(only the needed methods are listed here, but a real interface may have much more members, but not too much, to follow the interface segregation SOLID principle):
IUserRepository = interface(IInvokable) ['{B21E5B21-28F4-4874-8446-BD0B06DAA07F}'] function GetUserByName(const Name: RawUTF8): TUser; procedure Save(const User: TUser); end; ISmsSender = interface(IInvokable) ['{8F87CB56-5E2F-437E-B2E6-B3020835DC61}'] function Send(const Text, Number: RawUTF8): boolean; end;
Note also that all those code will use a plain record
as
Data Transfer Object (DTO):
TUser = record Name: RawUTF8; Password: RawUTF8; MobilePhoneNumber: RawUTF8; ID: Integer; end;
Here, we won't use TSQLRecord
nor any other
class
es, just plain record
s, which will be used as
neutral means of transmission. The difference between Data Transfer
Objects and business objects or Data Access Objects
(DAO) like our TSQLRecord
is that a DTO does not have any behavior
except for storage and retrieval of its own data. It can also be independent to
the persistency layer, as implemented underneath our business domain. Using a
record
in Delphi ensure it won't be part of a complex business
logic, but will remain used as value objects.
Now, let's come back to our TLoginController
class.
Here is the method we want to test:
procedure TLoginController.ForgotMyPassword(const UserName: RawUTF8); var U: TUser; begin U := fUserRepository.GetUserByName(UserName); U.Password := Int32ToUtf8(Random(MaxInt)); if fSmsSender.Send('Your new password is '+U.Password,U.MobilePhoneNumber) then fUserRepository.Save(U); end;
It will retrieve a TUser
instance from its repository, then
compute a new password, and send it via SMS to the user's mobile phone. On
success, it is supposed to persist (save) the new user information to the
database.
Why use fake / emulated interfaces?
Using the real implementation of IUserRepository
would expect a
true database to be available, with some potential issues on existing data.
Similarly, the class implementing ISmsSender
in the final project
should better not to be called during the test phase, since sending a SMS does
cost money, and we would need a true mobile phone or Internet gateway to send
the password.
For our testing purpose, we only want to ensure that when the "forgot my password" scenario is executed, the user record modification is persisted to the database.
One possibility could be to define two new dedicated class
es,
implementing both IUserRepository
and ISmsSender
interfaces. But it will be obviously time consuming and error-prone. This may
be a typical case when writing the test could be more complex than writing the
method to be tested.
In order to maximize your ROI, and allow you to focus on your business
logic, the mORMot framework proposes a simple and efficient way of
creating "fake" implementations of any interface
, just by defining
the minimum behavior needed to run the test.
Stubs and mocks
In the book "The Art of Unit Testing" (Osherove, Roy - 2009), a distinction is drawn between stub and mock objects:
- Stubs are the simpler of the two families of fake objects, simply
implementing the same interface as the object that they represent and returning
pre-arranged responses. Thus a fake object merely provides a set of method
stubs. Therefore the name. In mORMot, it is created via the
TInterfaceStub
generator; - Mocks are described as a fake object that helps decide if a test
failed or passed, by verifying if an interaction on an object occurred or not.
Everything else is defined as a stub. In mORMot, it is created via the
TInterfaceMock
generator, which will link the fake object to an existingTSynTestCase
instance - see this article.
In practice, there should be only one mock per test, with
as man stubs as necessary to let the test pass. Using a mocking/stubbing
framework allows quick on-the-fly generation of interface
with
unique behavior dedicated to a particular test. In short, you define the stubs
needed to let your test pass, and define one mock which will pass or fail the
test depending on the feature you want to test.
Our mORmot framework follows this distinction, by defining two
dedicated classes, named TInterfaceStub
and
TInterfaceMock
, able to define easily the behavior of such
classes.
Defining stubs
Let's implement our "forgot my password" scenario test.
The TSynTestCase
child method could start as such:
procedure TMyTest.ForgetThePassword; var SmsSender: ISmsSender; UserRepository: IUserRepository;
This is all we need: one dedicated test case method, and our two local variables, ready to be set with our stubbed / mocked implementation classes.
First of all, we will need to implement ISmsSender.Send
method.
We should ensure that it returns true
, to indicates a successful
sending.
With mORMot, it is as simple as:
TInterfaceStub.Create(TypeInfo(ISmsSender),SmsSender). Returns('Send',[true]);
It will create a fake class (here called a "stub") emulating the whole
ISmsSender
interface, store it in the local SmsSender
variable, and let its Send
method return true
.
What is nice with this subbing / mocking implementation is that:
- The "fluent" style of coding makes it easy to write and read the class
behavior, without any actual coding in Delphi, nor class definition;
- Even if ISmsSender
has a lot of methods, only Send
matters for us: TInterfaceStub
will create all those methods, and
let them return default values, with additional line of code needed;
- Memory allocation will be handled by the framework: when
SmsSender
instance will be released, the associated
TInterfaceStub
data will also be freed (and in case a mock, any
expectations will be verified).
Defining a mock
Now we will define another fake class, which may fail the test, so it is
called a "mock", and the mORMot generator class will be
TInterfaceMock
:
TInterfaceMock.Create(TypeInfo(IUserRepository),UserRepository,self). ExpectsCount('Save',qoEqualTo,1);
We provide the TMyTest
instance as self
to the
TInterfaceMock constructor
, to associate the mocking aspects with
this test case. That is, any registered Expects*()
rule will let
TMyTest.Check()
be called with a boolean
condition
reflecting the test validation status of every rule.
The ExpectsCount()
method is indeed where mocking is defined.
When the UserRepository
generated instance is released,
TInterfaceMock
will check all the Expects*()
rules,
and, in this case, check that the Save
method has been called
exactly one time (qoEqualTo,1
).
Running the test
Since we have all the expected stub and mock at hand, let's run the test itself:
with TLoginController.Create(UserRepository,SmsSender) do try ForgotMyPassword('toto'); finally Free; end;
That is, we run the actual implementation method, which will call our fake methods:
procedure TLoginController.ForgotMyPassword(const UserName: RawUTF8); var U: TUser; begin U := fUserRepository.GetUserByName(UserName); U.Password := Int32ToUtf8(Random(MaxInt)); if fSmsSender.Send('Your new password is '+U.Password,U.MobilePhoneNumber) then fUserRepository.Save(U); end;
This article is part of a list of mocking/stubbing range of information:
- Interfaces in practice: dependency injection, stubs and mocks
- Stubs and Mocks for Delphi with mORMot;
- Advanced mocks and stubs;
Feedback is welcome on our forum, just as usual.