Wikipedia describes them as "simulated objects that mimic the behavior of real objects in controlled ways". Other programming languages like Java and C# are using them for years and in fact also for Delphi there are some mocking frameworks. The problem with the existing frameworks in Delphi are that they either rely on generated code (like Delphi Mock Wizard) or strings for defining the expected method calls (like PascalMock).
In the past I already showed you some cool things I found in Emballo (which was developed in Delphi 2009 before the new enhanced RTTI was introduced). The author used a technique similar to what was introduced in Delphi XE in form of TVirtualMethodInterceptor but in a more limited way due to the lack of TValue and generics. With his permission I used this concept to create a more advanced version of mocking for Delphi. It was also inspired by NMock and in the following I will use the example from their tutorial to show you what you can do with DSharp Mock.
First let's see what it can do and what not. As it depends on TVirtualMethodInterceptor you can only mock virtual methods that can be seen by RTTI (public and published by default). And you can only use it in Delphi XE and higher. If you are using XE2 you can also mock Interfaces that contain RTTI (inherit them from IInvokable or add the $M+ directive) and have a guid.
In the following example I will use classes and virtual methods (the example from NMock uses interfaces but I also wanted to share this with those of you using XE).
We have a very simple scenario: a class (TAccountService) that can transfer money from one account to another using different currencies. The conversion rate is provided by another class (TCurrencyService). Both classes have abstract base classes (TAccountServiceBase and TCurrencyServiceBase). We now want to unit test our TAccountService without using the concrete TCurrencyService class (which does not even is part of this unit test). Let's take a look at the code:
interface
type
TAccountService = class(TAccountServiceBase)
private
FCurrencyService: TCurrencyServiceBase;
public
constructor Create(ACurrencyService: TCurrencyServiceBase);
procedure TransferFunds(
ASource, ATarget: TAccount; AAmount: Currency); override;
end;
implementation
constructor TAccountService.Create(ACurrencyService: TCurrencyServiceBase);
begin
FCurrencyService := ACurrencyService;
end;
procedure TAccountService.TransferFunds(
ASource, ATarget: TAccount; AAmount: Currency);
begin
ASource.Withdraw(AAmount);
ATarget.Deposit(AAmount);
end;
Our currency service base class looks as simple as this:
type
TCurrencyServiceBase = class
public
function GetConversionRate(AFromCurrency,
AToCurrency: string): Double; virtual; abstract;
end;
Now let's create our test method to check if the TransferFunds method works correct.
procedure TCurrencyServiceTest.TransferFunds_UsesCurrencyService;
var
LAmericanAccount: TAccount;
LGermanAccount: TAccount;
begin
LAmericanAccount := TAccount.Create('12345', 'USD');
LGermanAccount := TAccount.Create('54321', 'EUR');
LGermanAccount.Deposit(100);
FMockCurrencyService.WillReturn<Double>(1.38)
.Once.WhenCalling.GetConversionRate('EUR', 'USD');
try
FAccountService.TransferFunds(LGermanAccount, LAmericanAccount, 100);
Verify.That(LGermanAccount.Balance, ShouldBe.EqualTo<Double>(0));
Verify.That(LAmericanAccount.Balance, ShouldBe.EqualTo<Double>(138));
FMockCurrencyService.Verify();
finally
LAmericanAccount.Free();
LGermanAccount.Free();
end;
end;
First we are setting up 2 dummy accounts and deposit 100 euro on the german account.
After that it gets interesting. We define that the currency service will return 1.38 once when the method GetConversionRate gets called with the exact arguments.
After that we are calling the method we want to test. We are transferring 100 euro from the german account to the american account.
Then we want to check if this transfer went correct. So we are checking if the balance on the german account is 0 and the american account has a balance of 138 us dollars. You could use the regular Check methods of DUnit just like I did at first. Unfortunatly I ran into the problem with comparing floating numbers and the test failed for values that should be equal. This is because the DUnit CheckEquals method doesnt have overloads for all the floating types and it does not take the imprecision into account like for example the Math.SameValue functions. Also they are not easily to read in my opinion.
There is some extension for DUnit out there called DUnitLite that does something similar. Anyway also NMock has this Verify class that makes use of fluent interface syntax to make your checks more readable - almost like a real sentence. Internally it uses the DUnit exceptions so you will see some nice message when your check fails.
You probably already noticed that we were missing the call to the currency service so the transfer went wrong and we only have 100 us dollars on that account. Our unit test is telling us: "Expected: "? = 138" Actual: 100" That is what happens if you convert prices one to one. Good thing we would never do that and we fix that in our method.
procedure TAccountService.TransferFunds(
ASource, ATarget: TAccount; AAmount: Currency);
var
LConversionRate: Double;
LConvertedAmount: Currency;
begin
ASource.Withdraw(AAmount);
LConversionRate := FCurrencyService.GetConversionRate(
ASource.Currency, ATarget.Currency);
LConvertedAmount := AAmount * LConversionRate;
ATarget.Deposit(LConvertedAmount);
end;
Now our test runs successful. But there was one more line in our unit test we did not talk about yet. The Verify method checks if all expected method calls were made and none was missing. Any unexpected method call would have caused a failure immediately. For example if we swapped the currencies by mistake.
For the full syntax provided and how to set up mocks look at the sample in the repository. I am working on writing documentation for DSharp (thanks Paul for a Documentation Insight license) so it will be easier for you to understand and use in the future.
I think this possibility of writing your unit tests in a more declarative way without having to write a whole bunch of code just to mock dependencies makes writing tests more easy and less time consuming. Also reading them is much easier because you can just see what expectations are made on the dependencies and you can find out much easier what caused a test to fail.
As usual you find the source in the SVN repository.