Anzeige
Anzeige
Anzeige
Anzeige
Anzeige
Anzeige
Lesedauer 4 Min.

Software testen: Architektur

The Good, the Bad and the Ugly.
© EMGenie

Setzen Sie moderne Programmier-Paradigmen um und benutzen Sie eine saubere Architektur, so bekommen Sie eine testbare Anwendung geschenkt. Schreiben Sie eine testbare Anwendung, bekommen Sie die saubere Architektur geschenkt. Im Gegensatz zu einem Teufelskreis befinden Sie sich damit also in einem Engelskreis. Zumindest ergab eine oberflächliche Recherche zum Thema diesen Begriff als Gegensatz zum vicious circle. 

Die Quelltexte finden Sie im GitHub-Repository zu diesem Artikel im Branch „Architecture“.

Worum geht es?

Es soll ein GreetingService gebaut werden, der abhängig von der Tageszeit und dem aktuellen Benutzer eine Begrüßung ausgibt. Die Schnittstelle ist trivial, aber es gibt wenigstens eine Schnittstelle (Listing 1), was aus architektonischer Sicht definitiv ein Punkt auf der Haben-Seite ist.

Listing 1: Die Schnittstelle
public interface IGreetingService
{
    string SayHello();
} 

The Ugly

In Anspielung auf den oben zitierten Film soll es mit etwas Hässlichem losgehen, nämlich einer naiven Implementation der IGreetingService-Schnittstelle, an dem Sie als jemand, der dafür Unit-Tests schreiben soll, kaum ihre helle Freude haben werden. In Listing 2 sehen Sie den Quelltext (Quältext wäre hier wahrscheinlich zutreffender):

Listing 2: Der Hässliche
public class UglyGreetingService : IGreetingService
{
    public string SayHello()
    {
        var greeting = GetGreeting();
        var user = GetUser();
        return $"{greeting} {user.Name}";
    }

    private static User GetUser()
    {
        return new UserService().GetCurrentUser();
    }

    private static string GetGreeting()
    {
        var t = DateTime.UtcNow.TimeOfDay;
        if (t.TotalHours > 5 && t.TotalHours < 22)
        {
            return "Moin";
        }
        return "Slaap di wat";
    }
} 

Hässliche Probleme

Ohne Refaktorisierung können Sie diesen Dienst nicht sinnvoll testen:

  • GetCurrentUser gibt bestenfalls einen Leerstring oder null zurück, wenn es keinen angemeldeten Benutzer gibt.
  • GetGreeting ist direkt abhängig von der Tageszeit, also ist das Testergebnis ebenfalls abhängig von der Tageszeit.

 

Sie können maximal prüfen, ob SayHello eine Zeichenkette zurückgibt, aber das wäre reichlich überflüssig, denn über den Rückgabetyp gibt die Signatur von SayHello sowieso schon Auskunft. Sie haben weder Kontrolle über die aktuelle Tageszeit noch über den aktuellen Benutzer, der wahrscheinlich im Fall einer Webanwendung irgendwie aus irgendwelchen Authorisierungs-Headern des HttpContext des gerade ausgeführten HttpRequests geholt wird. Bevor Sie einen ziemlich sinnlosen Test wie den in Listing 3 schreiben, gehen Sie besser Kaffee holen.

Listing 3: Hässlicher Test (MSTest)
[TestClass]
public class UglyGreetingServiceTests
{
    private IGreetingService greetingService = null!;

    [TestInitialize]
    public void Setup()
    {
        greetingService = new UglyGreetingService();
    }

    [TestMethod]
    public void ReturnsGreeting()
    {
        var result = greetingService.SayHello();

        Assert.IsInstanceOfType<string>(result);
    }

    [TestCleanup]
    public void TearDown()
    {
        // Hier nur der Vollständigkeit halber. 
        // Gegebenenfalls erfolgt hier die Freigabe von Ressourcen oder das Aufräumen von Testdaten
        // Wird nach jedem Test aufgerufen
    }
} 

The Bad

Nachdem Sie Ihre Kollegen mit etlichen Fluchsalven und unzähligen WTFs amüsiert und vor allem Ihren Kaffee getrunken haben, machen Sie sich an die Arbeit. Hier tut eine beherzte Refaktorisierung Not (Listing 4). Sie beginnen mit dem UserService. Der bekommt eine Schnittstelle verpasst, sodass Sie mit einem Mock im Test bestimmen können, was GetCurrentUser zurückgibt. Die IUserService-Schnittstelle wird sodann als Konstruktor-Parameter dem GreetingService übergeben.

Listing 4: Der Böse
public class BadGreetingService(IUserService userService) : IGreetingService
{
    public string SayHello()
    {
        var greeting = GetGreeting();
        var user = GetUser();
        return $"{greeting} {user.Name}";
    }

    private User GetUser()
    {
        return userService.GetCurrentUser();
    }

    private static string GetGreeting()
    {
        var t = DateTime.UtcNow.TimeOfDay;
        if (t.TotalHours > 5 && t.TotalHours < 22)
        {
            return "Moin";
        }
        return "Slaap di wat";
    }
} 

Böse Probleme

Immerhin können Sie jetzt wenigstens etwas testen (Listing 5). Noch nicht schön, aber nicht mehr ganz so sinnbefreit wie vorher. Nach einem weiteren Kaffee und einigen eher verhaltenen Verwünschungen können Sie nun erste belastbare Ergebnisse Ihrer Arbeit einchecken. Nebenbei werden Sie bemerkt haben, das xUnit anstelle einer Setup-Methode den Konstruktor benutzt und dass zum Aufräumen die IDisposable-Schnittstelle verwendet wird.

Listing 5: Böser Test (xUnit)
public class BadGreetingServiceTests : IDisposable
{
    private IGreetingService greetingService = null!;

    public BadGreetingServiceTests()
    {
        // https://xunit.net/docs/shared-context
        var userService = new Mock<IUserService>();
        userService.Setup(u => u.GetCurrentUser()).Returns(new User(Guid.NewGuid(), "Christian"));
        greetingService = new BadGreetingService(userService.Object);
    }

    [Fact]
    public void ReturnsGreetingWithName()
    {
        var result = greetingService.SayHello();

        Assert.Contains("Christian", result);
    }

    public void Dispose()
    {        
        // Hier nur der Vollständigkeit halber. 
        // Gegebenenfalls erfolgt hier die Freigabe von Ressourcen oder das Aufräumen von Testdaten
        // Wird nach jedem Test aufgerufen
    }
} 

The Good

Nach einem weiteren Kaffee beschließen Sie, dass Sie noch Potenzial nach oben sehen. Das Testergebnis muss nun noch unabhängig von der Tageszeit gemacht werden (Listing 6). Sie stoßen auf die abstrakte Klasse TimeProvider. Da geht doch noch was!

Listing 6: Der Gute
public class GreetingService(IUserService userService, TimeProvider timeProvider) : IGreetingService
{
    public string SayHello()
    {
        var greeting = GetGreeting();
        var user = GetUser();
        return $"{greeting} {user.Name}";
    }

    private User GetUser()
    {
        return userService.GetCurrentUser();
    }

    private string GetGreeting()
    {
        var t = timeProvider.GetUtcNow().UtcDateTime.TimeOfDay;
        if (t.TotalHours > 5 && t.TotalHours < 22)
        {
            return "Moin";
        }
        return "Slaap di wat";
    }
} 

Gut

Sie sehen, dass Sie Tests parametrisieren können ([TestCase]-Attribute, Listing 7). Dazu aber in einem der nächsten Beiträge mehr. Sie sehen auch, dass xUnit im Gegensatz zu MSTest und NUnit kein Attribut benötigt, um eine Testklasse zu dekorieren. Aber am meisten freuen Sie sich, dass Ihre Tests nun auch unabhängig von der Tageszeit ein deterministisches Ergebnis zurückliefern! Wenn das keinen weiteren Kaffee wert ist…

Fragen Sie sich jetzt noch, wie Sie nun im wirklichen Leben beim Anwendungsstart eine konkrete Implementation des abstrakten TimeProviders bereitstellen, dann beachten Sie bitte folgenden Ausschnitt:

 

// services ist vom Typ IServiceCollection aus dem Namensraum Microsoft.Extensions.DependencyInjection
services.AddSingleton(TimeProvider.System);
Listing 7: Guter Test (NUnit)
[TestFixture]
public class GreetingServiceTests
{
    private IGreetingService greetingService = null!;    
    private readonly Mock<TimeProvider> timeProvider = new();

    [SetUp]
    public void Setup()
    {
        var userService = new Mock<IUserService>();
        userService.Setup(u => u.GetCurrentUser()).Returns(new User(Guid.NewGuid(), "Christian"));        
        greetingService = new GreetingService(userService.Object, timeProvider.Object);
    }

    [Test]
    public void ReturnsGreetingWithName()
    {
        var result = greetingService.SayHello();        

        Assert.That(result, Does.Contain("Christian"));
    }

    [TestCase(8, "Moin")]
    [TestCase(23, "Slaap di wat")]
    public void ReturnsGreetingWithTime(int hour, string expectedStart)
    {
        // arrange
        timeProvider.Setup(p => p.GetUtcNow()).Returns(new DateTime(2025, 6, 6, hour, 0, 0, DateTimeKind.Utc));
        
        // act
        var result = greetingService.SayHello();

        //assert
        Assert.That(result, Does.StartWith(expectedStart));
    }
} 

Fazit

Noch einen Kaffee? Leider ermöglicht der Alltag nicht immer so relativ einfache Möglichkeiten wie hier exemplarisch dargestellt. Aber um es mit einem Titel einer lesenswerten und unterhaltsamen Publikation von Eckard von Hirschhausen zu sagen: Die Leber wächst mit ihren Aufgaben. In diesem Sinne:

Bis neulich!

 

Neueste Beiträge

00:00
KI & Datenbanken: Was ändert sich wirklich?
Ein Interview mit Datenbankexperte Constantin Klein über die künftige Zusammenarbeit zwischen Datenbanken und Künstlicher Intelligenz.
11. Dez 2025
Adaptives Design mit der Uno Platform - Moderne UI-Gestaltung mit der Uno Platform, Teil 3
Als integraler Bestandteil des Cross-Plattform-Designs entlasten Adaptive UIs Entwicklerteams deutlich und bieten zugleich Anwendern ein hochwertiges Nutzungserlebnis.
7 Minuten
Simple Recipes – Schritt für Schritt zur Uno-App - Moderne UI-Gestaltung mit der Uno Platform, Teil 4
Der Entwicklungsweg einer modernen Cross-Platform-App: Von der Idee über UI-Skizzen bis zur finalen Implementierung zeigt ein praxisnahes Mini-Projekt am Beispiel der Rezepte-App „Simple Recipes“, wie Uno ein gemeinsames UI-Design für Desktop, Web und Mobile ermöglicht.
7 Minuten

Das könnte Dich auch interessieren

Bob lernt Fachjargon - Testdata-Builder, Teil 3
Vom Fluent-API zur Geschäftssprache – wie Domain-spezifische Sprachen Tests noch lesbarer und ausdrucksstärker machen.
6 Minuten
19. Nov 2025
Builder meets Faker - Testdata-Builder, Teil 2
Wer viele Testdaten braucht, liebt Bogus: Die Library erzeugt auf Knopfdruck realistische Daten und geht mit dem Builder-Pattern eine perfekte Kombination ein.
6 Minuten
12. Nov 2025
Hierarchische Testdata-Builder - Testdata-Builder, Teil 1
Lesbare Tests bei tiefen Objekthierarchien mit dem Collection-Builder-Pattern.
4 Minuten
Anzeige
Anzeige
Anzeige
Anzeige
Anzeige