For English Version see Mocking with Spring4D

In diesem Beitrag wollen wir uns das Mocking mit Spring4D ein wenig genauer anschauen. Die Basics zum Thema Mocking finden Sie hier.

Spring4D

Spring4D ist ein Open Source Framework für Delphi, dass neben den seit Version 1.2 enthaltenen Mockingklassen noch eine Menge weiterer Features bietet. Einer meiner Favoriten hier sind die generischen Interfaces für Listen, Kollektionen und Dictionaries oder die Multicast-Events. Ein „kleiner“ ORM-Mapper und ein cooles Dependency Injection Framework sind ebenfalls dabei.

Für die Installation laden Sie einfach das Archiv der gewünschte Version aus dem Repository herunter, entpacken die Archiv in einen Ordner, den Sie normalerweise für Ihre Komponenten verwenden und starten im Root-Ordner des entpackten Verzeichnisses die ausgelieferte Build.exe um das Framework zu compilieren. Wenn Sie die Checkbox „Update Registry“ markieren, erweitert Build.exe auch gleich ihrem Bibliothekspfad um die nötigen Einträge für Spring4D. Komponenten werden von Spring4D nicht registriert, die Klassen des Frameworks verwenden Sie nur im Code.

Mit Spring4D Mocks starten

Um mit den Mocks zu arbeiten, benötigen Sie lediglich die Unit Spring.Mocking in der uses-Anweisung. Die Unit enthält die meisten benötigten Strukturen. Kern des Frameworks ist ein generischer Record Mock<T>, welchen Sie für die zu ersetzenden Schnittstellen oder Klassen instantiieren. Für den Record sind die Operatoren für einige implizite Typecasts überladen, so dass der Record überall dort eingesetzt werden kann, wo die zu ersetzende Klasse oder Schnittstelle benötigt wird. Auf der anderen Seite können Sie dank der überladenen Operatoren auch aber auch generische Utility-Schnittstellen für das Mocking wie ISetup<T> oder IMock<T> verwenden.

Im vorherigen Artikel hatten wir erste Tests für die Methode Calculate der Klasse TShoppingBasket geschrieben. Die Methode Calculate berechnet den Wert eines Warenkorbs aus der Summe der Inhalte abzüglich der Werte der hinterlegten Rabattcodes. Das Problem war, dass diese Methode eine oder mehrere Schnittstellen verwendet. Die Schnittstelle ILogger für Logausgaben und die Schnittstelle IVoucherCalculator um die Werte von Gutscheincodes zu berechnen. Die Implementierungen für beide Schnittstellen werden TShoppingbasket im Konstruktor mitgegeben.

type
  TShoppingBasket = class(TObject)
  strict private
    FItemsCalculator: IVoucherCalculator;
    ...
  public
    constructor Create(const logger: ILogger; const customerId: string;
        const voucherCalculator: IVoucherCalculator);
    procedure AddVoucher(const code: string);
    procedure AddVouchers(const codes: TStringDynArray);
    ...
    procedure Calculate;
    property TotalValue: Currency read FTotalValue;
    property TotalDiscount : Currency read FTotalDiscount;
    property Hints: TStrings read FHints;
  end;

Die benutzte Schnittstelle ILogger wurde für den Test zunächst nur mit einem Stub ersetzt um die Berechnung einfacher Warenkörbe ohne Rabattgutscheine testen zu können. Da ILogger nur aufgerufen wird ohne Werte zu liefern, konnten wir diese Schnittstelle für den Test einfach durch einen Null-Logger versorgen. Für IVoucherCalculator macht das keinen Sinn. Die Schnittstelle sollte ja möglichst flexible Werte für Gutscheine zurückliefern, da ist uns nicht mit einem festen oder mehreren festen Werten geholfen, wie sie ein Stub zurückliefern würde.

Auf der andere Seite ist uns mit einer produktiven Implementierung hier auch nicht geholfen. Wir wollen ja hier gerade nicht zusätzlich testen, ob eine Implementierung von IVoucherCalculator tatsächlich funktioniert und beispielsweise doppelt verwendete Gutscheine zu Fehlern führen oder Gültigkeit und Wert korrekt ermittelt werden. Abgesehen davon wird eine produktive Implementierung der Schnittstelle mit ziemlicher Sicherheit wieder externen Ressourcen benutzen müssen um die Funktionen zu realisieren.

Mit Mock<T> lässt sich unser Problem lösen. Wir nehmen Spring.Mocking in die Uses-Anweisung auf und definieren in der Testklasse ein zusätzliches Feld FVoucherMock für den Mock:

TestTShoppingBasket_Calculate = class(TTestCase)
  strict private
    FShoppingBasket: TShoppingBasket;
    FVoucherMock: Mock<IVoucherCalculator>;
    ...

Dank der überladenen Operatoren für implizite Typecasts können wir den Record aus FVoucherMock direkt in der Setup-Methode unseres Tests an den Konstruktor unseres SUTs (System under test) übergeben:

procedure TestTShoppingBasket_Calculate.SetUp;
begin
   FShoppingBasket := TShoppingBasket.Create(TNullLogger.Create, '', FVoucherMock);
end;

Im Test selbst ist der Mock ähnlich einfach zu verwenden. In Spring4D sind die Schnittstellen und Aufrufe so gestaltet, dass ein einfaches Setup des Mocks später über die Fluent-Syntax als Einzeiler zu realisieren ist.

Rufen wir uns aber erst einmal die Deklaration der Schnittstelle in Erinnerung, die wir ersetzen wollen. Sie hat nur eine Methode, die von TShoppingBasket genutzt wird : CalculateVoucher.

type
  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 given voucher code</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;

Setup des Mocks

In einem ersten Test für die Methode Calculate können wir zunächst den einfachen Fall berücksichtigen, dass ein einzelner Gutscheincode hinterlegt ist. Wir würden analog anderen Tests einen Beispiel-Item im Warenkorb hinterlegen und zusätzliche einen Gutschein zufügen.

Unserem Mock müssen wir auch noch mit auf dem Weg geben, welchen Wert er für diesen Test zurückliefern soll. Das erledigen wir über die Setup-Eigenschaft des Mock-Rekords. Mit dem Eingabeparameter der generischen Methode Returns geben wir zunächst an, welchen Wert der Mock beim Aufruf zurückliefern soll – in diesem Fall den Wert der Konstante voucherValue.

procedure TestTShoppingBasket_Calculate.Test_OneVoucher;
const
  voucherCode = 'XDRET13';
  voucherValue = 20;
  singleItem : TBasketItem = (Count: 1;  Price: 200;);
begin
  // Arrange
  FShoppingBasket.AddItem(SingleItem);
  FShoppingBasket.AddVoucher(voucherCode);
  FVoucherMock.Setup.Returns<Currency>(voucherValue).When.CalculateVoucher(SingleItem.Price,'',voucherCode);
  // Act
  FShoppingBasket.Calculate;
  // Assert
  CheckEquals(sampleValue, FShoppingBasket.TotalDiscount);
end;

Praktischerweise liefert Returns darüberhinaus eine Schnittstelle IWhen<T> zurück mit deren Hilfe wir die Bedingungen definieren können unter welchen der eben angegeben Wert geliefert werden soll. Die Schnittstelle IWhen<T> ist übersichtlich und verfügt nur über eine Methode Namens When die uns den aktuellen Typen T des Mocks zurückliefert. Das versetzt die Programmierhilfe von Delphi in die Lage uns beim Aufruf zu helfen.

Indem wir die Methoden der Schnittstelle IVoucherCalculator im Mock-Setup mit bestimmten Parametern aufrufen, definieren wir die Bedingungen zu denen Returns den Eingabeparameter zurückliefert. Wird der Mock später vom SUT mit anderen Werten der Parameter von CalculateVoucher aufgerufen als denen, die wir angegeben haben, liefert der Mock stattdessen Default-Werte zurück. Wir müssen also nicht jeden Aufruf vorher einrichten, sondern diesen „Aufwand“ nur dann betreiben, wenn wir bestimmte Rückgabewerte benötigen. Umgekehrt können wir durch mehrere Aufrufe von Setup auch unterschiedliche Rückgabewerte für verschiedene Eingabewerte definieren, sofern das für den Test benötigt wird.

Das Verhalten testen

Der Test, den die oben gezeigte Methode Test_OneVoucher unseres Testcases implementiert, funktioniert. Der Wert, den wir in TotalDiscount prüfen entspricht unseren Erwartungen. Theoretisch könnte für den erfolgreichen Test aber auch eine falsche Implementierung verantwortlich sein. Der Test prüft bisher lediglich den Zustand nach Ausführung unseres Testobjektes. Was wir in dem Test noch nicht abprüfen ist, ob unser Mock überhaupt mit den Parametern gerufen wurde, wie wir das Erwarten, also ob unser Code sich so verhält wie wir das erwarten.

Andere Mockingframeworks bieten für den Mock meist eine Methode Verify an, die prüft, ob alle Methoden aus dem Setup des Mock auch gerufen wurden. Der Mock in Spring4D kennt diese Methode nicht und verfolgt einen etwas anderen Ansatz.

In Spring4D können wir die Prüfung des Verhaltens auf zwei Wegen erreichen. Zum einen können wir das grundsätzliche Verhalten unseres Mocks bei Aufrufen ändern und zum anderen können wir explizit die empfangene Aufrufe prüfen.

MockBehavoir

Geben wir nichts anderes an, wird ein Mock bei Spring4D im Modus „dynamic“ erzeugt. Das heißt der Mock akzeptiert alle Arten von Aufrufen mit beliebigen Parameter. Ist per Setup ein Verhalten für bestimmte Aufrufe und Parameterwerte definiert, wird dieses ausgeführt. Ist nichts speziell definiert, führt der Mock Standardverhalten aus. Werden vom Aufruf Rückgabewerte erwartet, liefert der Mock Defaultwerte – was bei Spring4D heißt: für einfache Datentypen wird 0, false oder leerer String geliefert für Schnittstellen aber beispielsweise nicht nil sondern ein Mock dieser Schnittstelle.

Der Mock kann auch in den Modus strict geschaltet werden. In diesem Modus wirft der Mock sofort eine Exception, wenn ein Aufruf erfolgt, der in dieser Form nicht vorher über Setup definiert wurde. Das Verhalten lässt sich über die Eigenschaft Behavoir des Mock jederzeit ändern. Unsere Verhaltensprüfung könnten wir damit durch diese kleine Erweiterung der oben gezeigten Testmethode erreichen

procedure TestTShoppingBasket_Calculate.Test_OneVoucher_Behavior;
...
  // Arrange
  ...
  FVoucherMock.Behavior := TMockBehavior.Strict;
  FVoucherMock.Setup.Returns<Currency>(voucherValue).When.CalculateVoucher(SingleItem.Price,'',voucherCode);
  // Act
  FShoppingBasket.Calculate;
  // Assert
  CheckEquals(voucherValue, FShoppingBasket.TotalDiscount);
end;

Wird das Strict-Behavoir für mehrere Tests benötigt, stellt der Mock-Record dafür auch eine statische Initialisierungsmethode Create zur Verfügung die im Testsetup verwendet werden könnte

// somewhere in Setup
FVoucherMock := Mock<IVoucherCalculator>.Create(TMockBehavior.Strict);

Received-Methode

Einen Alternative dazu, das Mockverhalten auf strict zu setzen, ist die Verwendung der Funktion Received des Mock. Die Funktion liefert ähnlich wie die Funktion When im Setup den Typen T zurück, für den der Mock instantiiert wurde. Entsprechend wird Received analog aufgerufen. Mit den angegebenen Funktionsparameter für den Aufruf hinter Received definieren Sie ihre Erwartung. Zusätzlich kann Received auch die erwartete Anzahl an Aufrufen mit dem optionalen Parameter vom Typ Times übergeben werden. Unter Verwendung von Received sieht unsere Verhaltensprüfung dann so aus:

procedure TestTShoppingBasket_Calculate.Test_OneVoucher_Received;
  ...
  // Arrange
  ...
  FVoucherMock.Setup.Returns<Currency>(voucherValue).When.CalculateVoucher(SingleItem.Price,'',voucherCode);
  // Act
  FShoppingBasket.Calculate;
  // Assert
  FVoucherMock.Received(Times.Once).CalculateVoucher(SingleItem.Price, '', voucherCode);
  CheckEquals(voucherValue, FShoppingBasket.TotalDiscount);
end;

Received kommt damit dem Verhalten von Verify recht nahe. Der entscheidende Unterschied ist, das Sie in Spring4D die Möglichkeit haben, das Setup und Prüfung unabhängig voneinander durchzuführen und für den jeweiligen Fall zu entscheiden, wie speziell oder allgemein die Prüfung sein soll.

Dieser Beitrag sollte einen ersten Überblick über das Mocking mit Spring4D bieten. Im folgenden Artikel werden wir uns anschauen wie wir mit Typecasts und Exceptions umgehen, Parameterwerte allgemeiner formulieren oder Aufrufreihenfolgen prüfen.

Links

Mocking in Spring4D
Markiert in: