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.
What a conicidence: http://www.finalbuilder.com/Resources/Blogs/tabid/458/EntryId/287/Introducing-Delphi-Mocks.aspx
ReplyDeleteFunny, isn't it? But we both did not copy from each other - great minds thing alike I guess ;)
ReplyDeleteI didn't even thought about one of you copying from the other. It is more likely that the necessary tools for such things are available now and people can to implement things that were difficult before, if not impossible at all.
ReplyDeleteActually I think it was possible before since Hallvard did that already more than 5 years ago and some things have been in Delphi for ages (all that soap stuff for example). It was just hard to use and required some advanced knowledge. Actually I think Vincent has the intention to implement the required classes also for down to Delphi 2010 to make such amazing techniques available for people that don't use the latest version of Delphi.
ReplyDeleteIt's definitely and idea who's time has come, XE & XE2 brought some of the missing enabling technology to the table. I would never have attempted to get something like this going before, although I have wanted to for many years. The real challenge is how to enable it now for earlier versions without using XE2's code ;)
ReplyDeleteFor compatibility with older Delphi versions, you can inspire from what PascalMock do http://pascalmock.cvs.sourceforge.net/viewvc/pascalmock/PascalMock/AutoMockIntf.pas?view=markup
ReplyDeleteI looked at that (PascalMockRio.pas where the actual work happens) and it basically does what recent versions of Delphi brought in the Rtti.pas (like creating the asm stubs to invoke the methods and much more). That's basically little beyond my knowledge tbh ;)
ReplyDelete