
Die Entwicklung moderner .NET-Anwendungen erfordert eine saubere und verlässliche Testbasis. Besonders in Systemen mit komplexer Geschäftslogik kann bereits eine kleine Codeänderung unerwartete Auswirkungen auf verschiedene Verarbeitungsschritte haben. Unit-Tests sorgen dafür, dass einzelne Komponenten isoliert geprüft und deren Verhalten unabhängig von Datenbank, Services oder externen Abhängigkeiten reproduzierbar bleibt.
Durch gezielten Einsatz von Mocking lassen sich selbst umfangreiche Berechnungsabläufe stabil testen. Fehlerquellen werden frühzeitig sichtbar, künftige Erweiterungen werden einfacher und sicherer umsetzbar. Dieser Beitrag zeigt, wie eine strukturierte Testarchitektur entsteht und wie sich Kernlogik zuverlässig automatisieren lässt.
In vielen bestehenden .NET-Anwendungen ist die Geschäftslogik stark mit externen Abhängigkeiten verbunden – etwa Datenbanken, Webservices oder fest codierte Konfigurationen. Dies erschwert die Umsetzung automatisierter Unit-Tests erheblich.
Insbesondere in Legacy-Code führt jede Änderung potenziell zu unbeabsichtigten Nebeneffekten. Solche Codebasen werden daher häufig gar nicht oder nur unzureichend getestet. Ein typisches Beispiel ist die Abhängigkeit von externen Services bei Berechnungen: Deren Ergebnisse lassen sich im Test nicht zuverlässig kontrollieren. Ohne klare Schnittstellen ist eine Trennung oder Simulation dieser Abhängigkeiten kaum möglich. Das resultiert in instabilen Tests oder ungetesteter Kernlogik.
Um Geschäftslogik effektiv testen zu können, müssen externe Abhängigkeiten konsequent entkoppelt werden. Ziel ist es, Berechnungslogik, Datenzugriffe und externe Services so zu kapseln, dass jede Komponente isoliert getestet werden kann. Dies gelingt durch den Einsatz klar definierter Interfaces in Kombination mit Dependency Injection.
Durch die Einführung schlanker Schnittstellen lassen sich externe Services gezielt mocken und kontrollierte Testergebnisse erzeugen. Bestehender Code muss dabei nur minimal angepasst werden – typischerweise auf Konstruktoren und Abhängigkeiten beschränkt. Das Ergebnis ist eine flexible Testarchitektur, die automatisierte Tests unterstützt und die Wartbarkeit des Systems nachhaltig verbessert.
Die zugrundeliegende Teststrategie basiert auf der vollständigen Isolation der Geschäftslogik. Externe Abhängigkeiten wie Datenbankzugriffe oder Webservice-Aufrufe werden nicht direkt aufgerufen, sondern über Interfaces abstrahiert. In Unit-Tests kommen Mocks zum Einsatz, um das Verhalten dieser Abhängigkeiten gezielt zu steuern und reproduzierbare Ergebnisse zu erzielen.
Für jeden relevanten Verarbeitungsschritt werden dedizierte Tests definiert. Dabei wird nicht nur geprüft, ob Methoden erfolgreich durchlaufen, sondern auch, ob fachliche Seiteneffekte korrekt ausgelöst werden – beispielsweise das Erzeugen von Datenbankeinträgen oder das Setzen von Statuswerten. Mithilfe von Callback-Mechanismen in den Mocks lassen sich diese Effekte verlässlich verifizieren, ohne auf eine echte Datenbank angewiesen zu sein.
Ein besonderes Augenmerk liegt auf der Nachvollziehbarkeit der Tests. Die Testdaten werden über strukturierte Hilfsklassen bereitgestellt, sodass sich Testszenarien gezielt erweitern und anpassen lassen. So entsteht eine wartbare Testbasis, die auch bei wachsender Komplexität stabil bleibt.
Praxisbeispiel: Testbare Geschäftslogik mit Mocking in .NET
Das folgende Beispiel zeigt eine typische Situation: Eine Geschäftslogik-Klasse berechnet eine Rechnungssumme auf Basis eines externen Wechselkurs-Services. Für Unit-Tests wird der Service über ein Interface abstrahiert und per Mock ersetzt. Zudem wird ein Repository-Mock verwendet, um Seiteneffekte wie das Speichern zu verifizieren.
1using System;
2namespace Demo.TestableBusinessLogic
3{
4 public interface IExchangeRateService
5 {
6 decimal GetRate(string fromCurrency, string toCurrency);
7 }
8 public interface IInvoiceRepository
9 {
10 void Save(Invoice invoice);
11 }
12 public sealed class Invoice
13 {
14 public string InvoiceNo { get; }
15 public decimal Amount { get; }
16 public string Currency { get; }
17 public Invoice(string invoiceNo, decimal amount, string currency)
18 {
19
20 InvoiceNo = invoiceNo;
21 Amount = amount;
22 Currency = currency;
23 }
24 }
25
26 public sealed class InvoiceResult
27 {
28 public decimal TotalEur { get; }
29 public bool Saved { get; }
30 public InvoiceResult(decimal totalEur, bool saved)
31 {
32 TotalEur = totalEur;
33 Saved = saved;
34 }
35 }
36
37// Kapselt die Berechnungslogik und delegiert externe Abhängigkeiten an Interfaces.
38
39 public sealed class InvoiceCalculator
40 {
41 private readonly IExchangeRateService exchangeRateService;
42 private readonly IInvoiceRepository invoiceRepository;
43 public InvoiceCalculator(IExchangeRateService exchangeRateService, IInvoiceRepository invoiceRepository)
44 {
45 this.exchangeRateService = exchangeRateService;
46 this.invoiceRepository = invoiceRepository;
47 }
48
49 // Berechnet den Rechnungsbetrag in EUR und speichert die Rechnung.
50
51 public InvoiceResult CalculateAndStore(Invoice invoice)
52 {
53 if (invoice == null)
54 throw new ArgumentNullException(nameof(invoice));
55 var rate = exchangeRateService.GetRate(invoice.Currency, "EUR");
56 var totalEur = Math.Round(invoice.Amount * rate, 2, MidpointRounding.AwayFromZero);
57 invoiceRepository.Save(invoice);
58 return new InvoiceResult(totalEur, true);
59 }
60 }
61}
62
63using Demo.TestableBusinessLogic;
64using Microsoft.VisualStudio.TestTools.UnitTesting;
65using Moq;
66
67namespace Demo.Tests
68
69{
70 [TestClass]
71 public class InvoiceCalculatorTests
72 {
73 // Prüft die korrekte Umrechnung sowie das Auslösen des Speichervorgangs.
74 [TestMethod]
75 public void CalculateAndStore_ConvertsAmountAndSavesInvoice()
76 {
77 var exchangeRateServiceMock = new Mock<IExchangeRateService>();
78 exchangeRateServiceMock
79 .Setup(x => x.GetRate("USD", "EUR"))
80 .Returns(0.90m);
81
82 Invoice savedInvoice = null;
83 var invoiceRepositoryMock = new Mock<IInvoiceRepository>();
84 invoiceRepositoryMock
85
86 .Setup(x => x.Save(It.IsAny<Invoice>()))
87 .Callback<Invoice>(i => savedInvoice = i);
88
89 var sut = new InvoiceCalculator(
90 exchangeRateServiceMock.Object,
91 invoiceRepositoryMock.Object);
92
93 var invoice = new Invoice("INV-1001", 100m, "USD");
94 var result = sut.CalculateAndStore(invoice);
95
96 Assert.AreEqual(90.00m, result.TotalEur);
97 Assert.IsTrue(result.Saved);
98 Assert.IsNotNull(savedInvoice);
99
100 exchangeRateServiceMock.Verify(x => x.GetRate("USD", "EUR"), Times.Once);
101 invoiceRepositoryMock.Verify(x => x.Save(It.IsAny<Invoice>()), Times.Once);
102 }
103 }
104}
Durch den Einsatz von Interfaces und gezieltem Mocking lässt sich Geschäftslogik vollständig isoliert testen. Externe Abhängigkeiten werden kontrollierbar, Testergebnisse bleiben reproduzierbar, und fachliche Seiteneffekte können präzise verifiziert werden. So entsteht eine robuste Testbasis, die Refactorings erleichtert und das Risiko unbeabsichtigter Nebenwirkungen deutlich reduziert.

Unser Geschäftsführer Tibor Csizmadia und unser Kundenbetreuer Jens Walter stehen Ihnen persönlich zur Verfügung. Profitieren Sie von unserer langjährigen Erfahrung und erhalten Sie eine kompetente Erstberatung in einem unverbindlichen Austausch.