For German Version see Unit-Tests mit Mocking
Why use mocks at all?
The name part „Unit“ in the unit test has nothing to do with the unit in Delphi – it is rather a matter of purposefully testing individual units and/or components of a program. If possible, classes that implement a functionality should be tested without their dependencies. Instead, we always would like to test the methods of a class in isolation in a unit test.
Normally, however, our classes do not work in isolation. They use services and methods from other classes either directly or via interfaces. As a developer you hopefully already considered the SOLID code concepts during the design phase. You write decoupled code and handle different responsibilities in different classes. But regardless of this, when trying to write isolated tests, you usually have problems if the classes you are using use resources or other business logic is called.
- Access to resources and external services on a test system or in the normal development environment often cannot be called fast enough or cannot be called at all. Sometimes it can be poorly set to a defined initial state (DB, Cloud Service etc)
- Error in the business logic called by a method influence the result of the unit tests of this method and thereby dilute the test result
What is Mocking
The term mocking usually refers to the replacement of productive code by special test implementations in a test scenario. Depending on what exactly implements or does not implement this replacement object, one can speak of stub, dummy, fake, spy or mock. A stub would be the simplest way to replace called code by simply providing empty methods – the null-object pattern for tests. Mock is the most complex form of a replacement implementation. A special implementation for testing purposes that not only logs processes, but also enables a correctness check. In addition, the term mock is also used as a generic term for all the different types of replacement implementations mentioned above
Manual mocking
You can also implement a mock manually without additional dependencies and frameworks. However, it is not only the effort that has a negative impact.
Let’s look at a simple example of a class that implements an imaginary shopping cart and has outsourced parts of the logic to other classes.
TShoppingBasket = class(TObject)
strict private
FItemsCalculator: IVoucherCalculator;
FLogger: ILogger;
FVoucherCodes: IList<string>;
FItems: IList<TBasketItem>;
...
strict protected
procedure DoChange; virtual;
public
constructor Create(const logger: ILogger; const customerId: string;
const voucherCalculator: IVoucherCalculator);
procedure AddVoucher(const code: string);
procedure AddVouchers(const codes: TStringDynArray);
procedure AddItem(const item: TBasketItem);
procedure AddItems(const items: TBasketItemsArray);
procedure Load(const loader: IBasketLoader);
procedure Calculate;
property TotalValue: Currency read FTotalValue;
property TotalDiscount : Currency read FTotalDiscount;
property Hints: TStrings read FHints;
end;
The class TShoppingBasket manages shopping basket entries and possible discount vouchers and calculates the total amount of the shopping basket minus the discounts granted. The class delegates some subtasks to other classes. So that the class can focus entirely on the management of the entire shopping basket, the checkup and evaluation of the discount codes is done by a separate class, which is addressed via the interface IVoucherCalculator . It is able to check each discount voucher and calculates its value if necessary.
EInvalidVoucher = class(ELogicalException);
/// <summary>interface to validate an calculate the code of a voucher</summary>
IVoucherCalculator = interface(IInvokable)
/// <summary>validates a voucher</summary>
/// <returns>value of voucher</returns>
/// <param name="itemsvalue">value of items in basket</param>
/// <param name="customerId">Unique Id for customer</param>
/// <param name="voucher">code for a voucher</param>
/// <exception cref="EInvalidVoucher">is raised if given voucher is invalid (code already used or invalid code etc)</exception>
/// <exception cref="ETechnicalException">are raised when calculation fails due to internal errors</exception>
function CalculateVoucher(const itemsvalue: Currency; const customerId, voucher: string): Currency;
end;
An implementation of the interface as well as a logger is provided via Constructor-Injection. If we want to use unit tests to check whether the Calculate class TShoppingBasket method works, we also need implementations of one or both dependencies, depending on the scenario. The dependency to an implementation of IVoucherCalculator is actually hidden one level deeper in the method CalculateDiscount and is not directly visible in the source of Calculate .
begin
ResetResults;
FTotalValue := GetItemsPrice;
FTotalDiscount := CalculateDiscount(FTotalValue);
// Limit the Discount
if FTotalDiscount < FTotalValue then
begin
FTotalValue := 0;
FTotalDiscount := FTotalValue;
FLogger.Warn('Discount bigger than Value');
end
else
FTotalValue := FTotalValue - FTotalDiscount;
end;
If we take a look at the code, it becomes immediately clear that we cannot test here without a logger. We can’t even calculate an empty shopping cart without giving the class a logger. This is where the first problems often start in practice. Only in rare cases does a logger store nothing – usually it will use the file system, a database or OS events. In the narrower sense, we are already leaving the field of unit testing and writing automated integration tests. So we need at least one „stub“ to call Calculate. We can realize this for example by writing a null-logger, an implementation of ILogger that contains nothing but empty method trunks:
TNullLogger = class(TInterfacedObject,ILogger)
public
procedure Log(level: TLogLevel; const msg: String);
procedure Error(const text: String; E: Exception = nil);
procedure Info(const text: String);
procedure Warn(const text: String);
procedure Debug(const text: String);
end;
...
procedure TNullLogger.Log(level: TLogLevel; const msg: String);
begin
// nothing to do
end;
We can then pass this implementation in the setup of a test instead of the correct logger and would now be able to write tests for simple scenarios without discount vouchers for the Calculate method.
begin
FShoppingBasket := TShoppingBasket.Create(TNullLogger.Create,'',nil);
end;
The simplest scenarios are an empty shopping cart or a shopping cart with one or more items
begin
// Arrange : nothing to do
FShoppingBasket.Calculate; // Act
CheckEquals(0,FShoppingBasket.TotalValue); // Assert
end;
procedure TestTShoppingBasket_Calculate.Test_SingleItem;
begin
FShoppingBasket.AddItem(SingleItem); // Arrange
FShoppingBasket.Calculate; // Act
CheckEquals(SingleItem.Price,FShoppingBasket.TotalValue); // Assert
end;
We follow a classic approach in our test szenarios above.We provide data and check the correct function on the basis of the data we read out in the test object or other objects afterwards. In such simple tests, we focus primarily on the state of the objects. But what if we are not only interested in the state before and after, but also in the behavior. Perhaps we would like to make sure in a test that warnings are issued in the log when an empty shopping cart is opened or that certain interfaces are called. In this case we don’t get ahead with simple stubs, but have to design specific implementations for the test, which are either relatively specific and simple or correspondingly more complex, when realized more generally. In the case of simply implemented mock objects, more and more logic moves from the test method to the separate class. This makes the test more difficult to read because its setup, the execution and checking of results no longer stand side by side (AAA scheme Arrange-Act-Assert).
Automatic mocking
Already for meanwhile antiquated Delphi versions without Unicode there was a small mocking framework called Pascalmock that could be used to write simple mocks based on Variants. Generic types and the additional RTTI functions of Delphi 2010 have created new possibilities here and so you as Delphi developer now have the choice between several mocking frameworks, all of which offer automatic mocking. dSharp and DelphiMocks were developed at the same time and some time later the open source framework Spring4D was extended by automatic mocking. There are already blog articles about dSharp as well as DelphiMocks on the net that provide an introduction, for Spring4D the following article provides an introduction.