Net-Base Dergi

12.09.2026

DUnitX ile Birim Testleri: Bağımlılıkları Interface üzerinden mocklamak, projeyi bozmadan

Delphi içinde DUnitX ile birim testlerini nasıl kuracağınız, bağımlılıkları Interface üzerinden nasıl mocklayacağınız ve bu sırada domain mantığını altyapıdan temiz şekilde nasıl ayıracağınız – test anahtarı ve Uses-kaosu olmadan.

12.09.2026

Dergi konusundan proje pratiğine

İçeriğe Uygun Hizmet ve Teknik Sayfalar

Sie klicken in der IDE auf „Tests ausführen“ – und hören am Laptop-Lüfter, dass gerade mehr passiert als eine Preisregel oder Validierung. In der Konsole tauchen DB-Connects auf, ein Pfad wird aus einer INI gelesen, irgendwo versucht ein HTTP-Client eine Gegenstelle zu erreichen. Und wenn der Lauf dann auch noch von der Uhrzeit abhängt, haben Sie ein sehr typisches Problem: Die Tests sind nicht isoliert, sondern ziehen Infrastruktur mit.

Genau hier setzt das Thema Unit-Tests mit DUnitX an: Abhängigkeiten per Interface mocken, ohne das Projekt zu verbiegen. „Verbogen“ ist ein Projekt meist dann, wenn Produktionscode Test-Schalter bekommt, wenn Sichtbarkeiten nur für Tests aufgeweicht werden oder wenn das Testprojekt am Ende die halbe UI und Datenzugriffsschicht mitschleppt. Der saubere Weg ist unspektakulär: klare Grenzen, kleine Interfaces, ein bewusstes „Zusammenstecken“ (Composition Root) – und Mocks/Fakes nur dort, wo sie fachlich wirklich helfen.

Erste Prüffrage: Ist das wirklich ein Unit-Test oder schon Integration?

Arbeitsplatzfoto mit Debugging-Situation und handgezeichnetem Architektur-Sketch ohne lesbaren Text
Wenn Tests Infrastruktur ziehen, sieht man es oft zuerst in unerwarteten Aufrufen und Abhängigkeiten.
Abstrakte Grafik eines Schichtenmodells mit Interface-Knoten zwischen Domäne und Infrastruktur
Entscheidende Schnittstelle: Domänenlogik bleibt innen, Infrastruktur außen.

Bevor Sie über Mocking sprechen, lohnt eine knappe Diagnose. Ein Unit-Test sollte ohne Netzwerk, Dateisystem, Datenbank und ohne „echte Zeit“ laufen – und bei gleichem Input immer das gleiche Ergebnis liefern. Wenn das nicht erfüllt ist, ist der Test nicht „schlecht“, aber er ist sehr wahrscheinlich ein Integrations- oder Systemtest. Der Unterschied ist nicht akademisch: Er entscheidet über Laufzeit, Stabilität, Fehlersuche und CI-Tauglichkeit.

Praktische Kriterien, die sich im Alltag bewähren:

  • schnell: Millisekunden bis wenige Dutzend Millisekunden; bei hundert Tests soll ein Lauf nicht zur Kaffeepause werden,
  • isoliert: keine echten externen Ressourcen, keine Abhängigkeit von Rechnerzustand, Pfaden oder Accounts,
  • diagnostizierbar: der Fehler zeigt fachlich auf eine Regel, nicht auf „Timeout“ oder „Zugriff verweigert“.

Wenn Ihr Test diese Eigenschaften verfehlt, ist „mehr Mocking“ selten die Lösung. Häufig ist die Grenze zwischen Domänenlogik (berechenbar) und Infrastruktur (I/O, Zeit, externe Systeme) nicht sauber gezogen.

DUnitX richtig einordnen: Runner und Struktur – Mocking kommt von außen

DUnitX liefert Ihnen den Rahmen für Tests: Fixtures, Attribute, Assertions, Reporting und den Runner. In der RAD-Studio-Dokumentation wird DUnitX als Open-Source Unit-Test-Framework beschrieben, das sich an NUnit/xUnit-Konzepten orientiert[Quelle]. Der praktische Nutzen daran: Sie bekommen eine wiedererkennbare Teststruktur und einen standardisierten Ablauf – ohne dass Ihre Anwendung „Testmodus“ spielen muss.

Wichtig für den Betrieb von Tests: DUnitX-Projekte laufen typischerweise als eigene Test-Executable (Konsolen- oder GUI-Runner). Das entkoppelt den Testlauf von Ihrer produktiven Anwendung und macht CI einfacher, weil Tests als separater Prozess laufen können[Quelle]. Das beantwortet aber noch nicht die Abhängigkeitsfrage.

Mocking ist in DUnitX nicht „eingebaut“. In Delphi ist es üblich, DUnitX mit einem Mocking-Framework zu kombinieren oder bewusst Fakes/Stubs handzuschreiben. Delphi-Mocks beschreibt sich als Open-Source Mocking-Framework für Delphi (ab XE2 oder später)[Quelle]. Ein belastbarer Praxisindikator: In den eigenen Tests von DUnitX wird Delphi.Mocks eingebunden und genutzt (z. B. in Logger-Tests)[Quelle]. Das ist kein „Sie müssen es so machen“, aber es belegt, dass die Kombination im echten Code funktioniert.

Mock, Fake oder Stub: weniger Dogma, mehr Wartbarkeit

Im Team-Jargon ist alles schnell ein „Mock“. Für stabile Tests lohnt die genauere Unterscheidung, weil sie direkt beeinflusst, wie fragil ein Test wird.

  • Stub: liefert vordefinierte Rückgaben. Keine oder kaum Verifikation. Gut für „Input rein, Output raus“.
  • Fake: einfache, aber echte Implementierung, oft In-Memory (z. B. Repository im RAM). Gut, wenn Verhalten über mehrere Aufrufe konsistent sein muss.
  • Mock: erlaubt Erwartungen und Verifikation (Aufrufanzahl, Parameter, ggf. Reihenfolge). Gut für fachlich relevante Seiteneffekte.

Delphi-spezifisch ist der Effekt spürbar: Viele „flaky“ Tests entstehen nicht, weil die Logik komplex wäre, sondern wegen Lebenszyklen (Interface-Referenzzählung), globaler Zustände oder zu strenger Verifikation. In solchen Situationen sind Fakes oft robuster als Mocks, weil sie weniger an der Aufrufchoreografie hängen.

Entscheidungsraster: Welche Abhängigkeiten zuerst isolieren?

Wenn Sie in Bestandssoftware starten, können Sie nicht alles auf einmal abstrahieren. Sinnvoller ist ein Schritt-für-Schritt-Ansatz entlang der Abhängigkeiten, die Tests langsam und unzuverlässig machen. Das Raster hilft, nicht in „Interface-Overengineering“ zu kippen.

Abhängigkeit Typische Symptome ohne Isolierung Passende Interface-Grenze Empfohlener Ersatz im Unit-Test
Zeit (Now/Date, Timer) Tests brechen am Tageswechsel, Monatsabschluss, Sommerzeit ITimeProvider / IUhr Stub mit festem Timestamp
Konfiguration (INI/Registry/ENV) „Geht nur auf meinem Rechner“, Pfade/Keys fehlen IConfig Fake (Dictionary-basiert)
Dateisystem Locking, Rechte, Cleanup, temp-Verzeichnis abhängig IFileSystem Fake (In-Memory) oder Stub
Datenbank / Persistenz Langsame Läufe, Testdatenpflege, Seiteneffekte durch Transaktionen IRepository / IUnitOfWork Fake (In-Memory) oder Mock (Commit/Save verifizieren)
HTTP/REST, externe Services Flaky durch Netzwerk, Proxy, TLS, Rate-Limits IApiClient / IHttpClient Stub/Mock mit vordefinierten Responses
Logging Testausgaben „vermüllen“, Fehlersuche unklar ILogger Fake-Collector oder Mock (nur bei Bedarf)

Lesart des Rasters: Es geht nicht darum, jede Infrastruktur sofort zu „verstecken“. Es geht darum, Domänenlogik von I/O zu entkoppeln, damit Sie Regeln in Millisekunden prüfen können. Die Infrastruktur selbst bekommt dann gezielte Integrations-Tests.

Unit-Tests mit DUnitX umsetzen: Interface-Grenzen ohne Test-Schalter

„Projekt verbiegen“ passiert in Delphi oft in diesen Mustern:

  • Compiler-Direktiven oder Test-Flags im Produktionscode („wenn Testmodus, dann …“).
  • Zusätzliche öffentliche Methoden, nur damit Tests „rankommen“ (API-Oberfläche wächst, Kapselung sinkt).
  • Tests, die konkrete Implementierungs-Units brauchen und dadurch Uses-Kaskaden auslösen.

Der saubere Ansatz bleibt im Produktionscode ruhig: Abhängigkeiten werden über Interfaces übergeben (Injection), produktive Defaults werden an einer klaren Stelle zusammengebaut (Composition Root). Das kann beim Programmstart der Desktop-App sein, beim Start eines Windows-/Linux-Services oder beim Erzeugen von Request-Handlern im REST-Server. Entscheidend ist: Die Domänenklasse selbst sollte nicht „heimlich“ konkrete Adapter instanziieren.

Schrittfolge: Von hart verdrahtet zu testbar – in kleinen, sicheren Schnitten

  1. Hotspot wählen: Eine Klasse oder Funktion, die im Testlauf I/O triggert (DB, Files, HTTP, Zeit). Nicht „das ganze System“ anfassen.
  2. Abhängigkeit fachlich benennen: „Kunden-Repository“ statt „FDQuery“, „Zeitquelle“ statt „Now“.
  3. Interface minimal schneiden: Nur die Operationen, die die Logik wirklich braucht. Große Sammel-Interfaces sind teuer.
  4. Adapter bauen: Die produktive Implementierung kapselt BDE-Ablosung mit nativer Anbindung/HTTP/Datei-API hinter dem Interface, ohne die Logik umzuschreiben.
  5. Injection einführen: Konstruktor- oder Property-Injektion. Default-Instanziierung nur im Composition Root, nicht in der Domänenklasse.
  6. Ersten Test schreiben: Der Test muss ohne I/O laufen. Wenn das nicht gelingt: Grenze nachschärfen, nicht „mehr Setup“ bauen.
  7. Erweitern nur bei Bedarf: Interface-Methoden erst hinzufügen, wenn ein weiterer Use-Case sie wirklich braucht.

Konstruiertes Beispiel: „Nur kurz testen“ – und plötzlich hängt es am DB-Connect

Der Knackpunkt im Beispiel: Sie mocken nicht „Technik“, Sie isolieren fachliche Inputs. Damit bleibt die Preislogik deterministisch. Und Sie können später Integrations-Tests gezielt dort schreiben, wo sie hingehören: am Repository/Config-Adapter.

Typische Delphi-Fallstricke beim Interface-Mocking (und wie Sie sie erkennen)

1) Interface-Lebensdauer und Referenzzählung

Mocks und Fakes hängen in Delphi oft an Interfaces – damit an Referenzzählung. Fehlerbilder sind unangenehm: AVs, weil ein Objekt zu früh freigegeben wurde, oder Leaks, weil Referenzzyklen entstehen.

  • Vermeiden Sie zyklische Referenzen über Interfaces (A hält B, B hält A). Wenn nötig: Ownership klar machen und den Zyklus aufbrechen.
  • Erzeugen Sie Mocks pro Test und halten Sie sie so, dass sie sicher bis zum Ende der Testmethode leben (z. B. als Felder der Fixture).

2) Globale Singletons: „Ich injiziere doch schon“ – nur nicht konsequent

Ein häufiger Randfall: Sie injizieren an der Oberfläche ein Interface, aber tiefer im Call-Stack wird wieder direkt auf Now, eine globale Config oder eine Singleton-DB zugegriffen. Das Ergebnis sind Tests, die unerwartet I/O machen oder nur in einer bestimmten Reihenfolge laufen.

Debugging-Hinweis: Wenn ein Unit-Test „sporadisch“ fehlschlägt, setzen Sie Breakpoints auf die Infrastruktur-Einstiege (DB-Connect, FileOpen, HTTP Execute). Der erste Treffer zeigt nicht selten den echten Übeltäter: eine Stelle, die die Abhängigkeit umgeht.

3) Zu strenge Verifikation: Tests werden zu „Implementierungs-Polizei“

Mocks können Aufrufanzahl, Parameter und teilweise Reihenfolge verifizieren. Das ist nützlich, aber gefährlich. Der Test fällt dann bei jeder internen Optimierung, obwohl das fachliche Ergebnis identisch ist.

Eine robuste Regel: Verifizieren Sie Interaktionen nur, wenn es fachlich ein Vertrag ist (z. B. „Commit muss passieren“, „E-Mail darf nur einmal gesendet werden“, „Audit-Event ist Pflicht“). Für alles andere sind Ergebnis-Assertions stabiler.

Projektstruktur im Griff: Test-Executable, Uses und Build-Zyklen

In Delphi kippt Testbarkeit oft über Unit-Abhängigkeiten. Ein Testprojekt sollte idealerweise:

  • als eigene Executable laufen (DUnitX-Runner),
  • die Domänen-Units und Interface-Units referenzieren, nicht die komplette UI-Schicht,
  • keine „magischen“ Pfade voraussetzen (z. B. Dateien relativ zum Projektverzeichnis, die im CI fehlen).

Wenn Ihr Testprojekt plötzlich VCL/FMX, Datenbankkomponenten oder große Utility-Bündel zieht, ist das ein Signal: Die Grenze ist nicht sauber. Häufig hilft eine gezielte Umordnung: eine kleine Unit (oder ein kleines Paket) für Interfaces/DTOs (Datentransferobjekte) und eine Domänen-Unit, die keine Infrastruktur-Uses hat. Die Adapter liegen separat.

Legacy-Delphi und „Uses-Hölle“: Warum Interfaces hier besonders wirken

Viele gewachsene Delphi-Systeme haben globalen Zustand, viele Uses und starke Kopplung. Das verstärkt Seiteneffekte und macht Refactoring riskant. Als typische Testbremse werden genau diese Muster benannt: globale Zustände, viele Uses und starke Unit-Abhängigkeiten[Quelle]. Die Grenze der Quelle: Sie liefert keinen universellen Umbauplan. Die Folgerung für Unit-Tests ist aber klar: Sie brauchen eine Abhängigkeitsrichtung, die nicht ständig in Infrastruktur zurückgreift.

Pragmatisches Zielbild: Ein schmaler Ring aus Interfaces trennt Domäne und Adapter. Hinter dem Ring darf es „legacy“ bleiben. Wichtig ist, dass die Domäne nicht mehr gezwungen ist, Infrastruktur zu kennen.

DUnitX-Lifecycle: Setup/Teardown korrekt verstehen, sonst werden Tests flakey

Ob Setup/Teardown pro Test oder pro Fixture läuft, wie der Runner instanziiert und ausführt – das ist keine Nebensache. In der Praxis führen Missverständnisse hier zu Zustands-Leaks zwischen Tests und zu Effekten, die lokal „zufällig“ gutgehen. Dass es hier Stolperstellen gibt, sieht man auch an wiederkehrenden Fragen in DUnitX-Issues, etwa zur erwarteten Häufigkeit von Setup/Teardown-Aufrufen[Quelle]. Eine Issue-Diskussion ist keine Norm, aber sie ist ein gutes Warnsignal: Viele Teams rennen in denselben Randfall.

Konsequenzen, die Tests spürbar stabiler machen:

  • Behandeln Sie jede Testmethode als isoliert. Erzeugen Sie Zustand pro Test neu oder setzen Sie ihn im Setup konsequent zurück.
  • Vermeiden Sie geteilte Mocks über mehrere Tests, wenn Sie nicht exakt wissen, wie der Lifecycle im Runner läuft.
  • Wenn Tests parallel laufen: keine globalen Ressourcen, keine statischen Caches ohne Synchronisation, keine Singletons mit verstecktem Zustand.

Debugging-Leitfaden: Wenn der Mock „nicht greift“

Wenn trotz Mock plötzlich die produktive Implementierung aufgerufen wird, steckt meist einer dieser Gründe dahinter:

  • Die Klasse erzeugt Abhängigkeiten intern: Im Konstruktor oder in einer Hilfsmethode wird ein Adapter instanziiert, statt ihn zu übernehmen.
  • Das Interface wird nicht durchgereicht: Ein Layer nimmt die Abhängigkeit entgegen, baut darunter aber wieder konkrete Objekte.
  • Es gibt mehrere Wege zur Ressource: Ein Teil nutzt Repository, ein anderer greift direkt auf eine globale Connection zu.
  • Default-Verhalten bei nil: Optionaler Parameter oder Property führt bei nil zur Default-Erzeugung. Bequem, aber testfeindlich.

Ohne Komplettumbau helfen oft zwei schnelle Maßnahmen:

  • Breakpoints im produktiven Adapter an den I/O-Einstiegen (Connect/Execute/FileOpen). Wenn der Breakpoint im Unit-Test hitten kann, ist die Grenze nicht dicht.
  • Testausgaben kontrollieren: Ein Fake-Logger als Collector zeigt, ob unerwartete Infrastrukturpfade genutzt wurden, ohne dass Sie auf Konsole/Dateien angewiesen sind.

Wann lohnt sich der Aufwand – und wann sind andere Tests sinnvoller?

Interface-Mocking lohnt sich besonders, wenn Sie Logik haben, die häufig angepasst wird oder deren Fehler spät und teuer auffallen. Typische Kandidaten in Business-Software: Preisregeln, Freigaben, Plausibilitäten, Buchungslogik, Statusautomaten, Berechtigungsentscheidungen.

Weniger sinnvoll ist der Aufwand, wenn Sie am Ende nur Verhalten externer Bibliotheken „nachtesten“ oder wenn der reale Fehler in der Integration liegt: SQL-Dialekt, Transaktionen, Encoding, Zertifikate/Proxy, Dateisperren, Threading. In diesen Fällen ist ein klar getrennter Integrations-Test oft ehrlicher und spart Zeit, weil er den echten Adapter gegen echte Infrastruktur prüft.

Ein stabiler Zielzustand: Domäne per Unit-Tests abgesichert, Adapter separat geprüft

Ein praktikables Bild für viele Delphi-Unternehmensanwendungen sieht so aus:

  • Unit-Tests für Domänenlogik: keine DB/HTTP/Files, Inputs über Interfaces (Mocks/Fakes/Stubs).
  • Integrations-Tests für Adapter: Repository gegen echte Datenbank, API-Client gegen Testserver, File-Adapter gegen temporäre Verzeichnisse.
  • Smoke-Tests für den Gesamtschnitt: Start/Initialisierung plus ein bis zwei kritische End-to-End-Flows.

Damit bauen Sie kein „Testkonstrukt“, sondern machen Systemgrenzen sichtbar. Und Sie bekommen eine Testlandschaft, die sowohl schnelle Rückmeldung als auch echte Sicherheit liefert.

Schlusspunkt: Das Interface ist das Werkzeug – die Grenze ist die eigentliche Arbeit

Wenn Unit-Tests mit DUnitX in Ihrem Projekt schwerfallen, ist das selten ein Framework-Problem. Häufig ist es Kopplung: Infrastruktur steckt in der Logik, globale Zustände machen Ergebnisse nicht deterministisch. Interface-basierte Abhängigkeiten sind der kleinste, saubere Hebel, um diese Kopplung zu lösen. Starten Sie mit wenigen, harten Abhängigkeiten (Zeit, Konfiguration, Persistenz), halten Sie Interfaces klein und testen Sie fachliche Ergebnisse statt Aufrufchoreografie. Dann werden Tests schnell, verständlich – und im Debugging wirklich hilfreich.

Wenn Sie beim Aufteilen in Domäne und Adapter unsicher sind oder eine gewachsene Delphi-Anwendung schrittweise testbar machen wollen: Projekt oder Modernisierungsvorhaben mit Net-Base besprechen.

Quellen und weiterfuehrende Informationen

Die fachlichen Kernaussagen wurden anhand der folgenden externen Quellen redaktionell eingeordnet.

  1. Überblick über DUnitX – RAD Studio (docwiki.embarcadero.com)
    DUnitX ist als Open-Source Unit-Test-Framework in RAD Studio dokumentiert und wird typischerweise als eigenständige Test-Executable (Runner) genutzt.
  2. GitHub – VSoftTechnologies/Delphi-Mocks: A Open Source Mocking framework for Delphi XE2 or later · GitHub (github.com)
    Delphi-Mocks beschreibt sich als Open-Source Mocking-Framework für Delphi (ab XE2 oder später) und ist eine gängige Ergänzung zu DUnitX.
  3. DUnitX/Tests/DUnitX.Tests.Loggers.XML.NUnit.pas at master · VSoftTechnologies/DUnitX · GitHub (github.com)
    DUnitX verwendet in eigenen Tests Delphi.Mocks, was die praktische Kombinierbarkeit von DUnitX und Interface-Mocking belegt.
  4. Legacy-Code in Delphi refactoren | Net-Base (net-base-software-gmbh.de)
    In Legacy-Delphi-Kontexten bremsen globale Zustände und starke Unit-Abhängigkeiten die Testbarkeit; Interfaces helfen, Kopplung zu reduzieren.
  5. SetUp/TearDown exactly once for all test methods in a TestCase ? · Issue #311 · VSoftTechnologies/DUnitX (github.com)
    Fixture-Lifecycle und Setup/Teardown-Erwartungen sind in der Praxis eine typische Stolperfalle, erkennbar an wiederkehrenden Diskussionen rund um DUnitX.

Für dieses Thema sind auch Delphi Unit-Tests und Interface Mocking Delphi wichtig. Der Beitrag ordnet diese Aspekte verständlich ein und zeigt, worauf es im Alltag ankommt.

Projekt oder Modernisierungsvorhaben mit Net-Base besprechen.

Sonraki adım

Konu gerçek bir projeye dönüştüğünde, mimari, mevcut sistemler ve işletme erken dönemde birlikte değerlendirilmelidir.

Bireysel sorularda destek vermekle kalmıyoruz; kaynak kodu parçacıklarından, legacy konularından veya portal fikirlerinden sağlam bir kurumsal projeye dönüşene kadar da destek veriyoruz.

  • Mevcut durum, hedef durum ve teknik riskler birlikte değerlendirilir.
  • REST, veri erişimi, portallar ve Rollout daha sonra ortaya çıkan sonuçlar olarak ertelenmez.
  • Hangi yolun ekonomik ve işletme açısından sürdürülebilir olduğunu erken görürsünüz.

Gönderiyi paylaş

Bu gönderiyi doğrudan paylaş

LinkedIn, X, XING, Facebook, WhatsApp ve e-posta hemen kullanılabilir. Instagram için bağlantıyı ve kısa metni doğrudan hazırlıyoruz.

E-posta

Instagram yeni bir sekmede açılır. Bağlantı ve kısa metin önceden panoya kopyalanır.