Bild von Stefan Lieser
Stefan Lieser

Einfache Refactorings – Teil 5

Bei diesem Beitrag handelt es sich um eine überarbeitete Version. Er wurde zuvor bereits bei refactoring-legacy-code.net veröffentlicht.

Dies ist eine Beitragsserie. Die weiteren Beiträge finden Sie hier:

Die Testbarkeit verbessern – Internal Test Constructor

 

Um automatisierte Tests ergänzen zu können, ist es manchmal sinnvoll, einen Internal Test Constructor mit einem Konstruktorparameter einzuführen, über den die Abhängigkeit von außen reingereicht werden kann. Die folgende Methode kann nicht gut automatisiert getestet werden, da sie eine Abhängigkeit von der aktuellen Zeit, hier in Form der DateTime.Now Eigenschaft, hat.

public string Format(string message) {
    return DateTime.Now + ": " + message;
}

Die Testbarkeit kann hergestellt werden, indem der Aufruf der statischen DateTime.Now Methode über eine Func beeinflussbar gemacht wird. Dazu führen Sie zunächst ein Feld ein:

private Func now = () => DateTime.Now;

Anschließend ersetzen Sie alle DateTime.Now Aufrufe durch Aufrufe von now():

public string Format(string message) {
    return now() + ": " + message;
}

Dieses Refactoring ist nicht vollständig toolgestützt durchführbar, insofern ist hier Vorsicht geboten. In jedem Fall sollten Sie vor diesem Refactoring den Stand des Quellcodes in der Versionskontrolle ablegen. Ferner sollten Sie den Stand nach dem Refactoring dort ablegen, so dass die Differenz in der Versionskontrolle exakt dieser Veränderung am Quellcode entspricht. So ist es möglich, diesen Schritt später wieder rückgängig zu machen, sollte sich ein Fehler eingestellt haben.

Um nun das Verhalten von außen im Test beeinflussen zu können, machen Sie das Feld now über den Internal Test Constructor zugänglich. Damit sich durch den zusätzlichen Konstruktor das Verhalten des restlichen Softwaresystems nicht verändert, ergänzen Sie zusätzlich einen parameterlosen Defaultkonstruktor, der die Lambda Expression zur Initialisierung des Feldes an den internen Test Konstruktor übergibt.

public class FormattingService
{
    private readonly Func now;

    public FormattingService() : this (() => DateTime.Now) {
    }

    internal FormattingService(Func now) {
        this.now = now;
    }

    public string Format(string message) {
        return now() + ": " + message;
    }
}

Fazit

 

Führen Sie einen zusätzlichen Konstruktor mit Parametern ein, um die Testbarkeit einer Klasse zu erhöhen.

Abhängigkeiten reduzieren – Extract Interface

 

Häufig existieren in Legacy Code Projekten direkte Abhängigkeiten zwischen Klassen. Dadurch wird das automatisierte Testen erschwert. Durch die Einführung eines Interface mit dem Extract Interface Refactoring kann im Test leichter mit Attrappen gearbeitet werden. Im folgenden Beispiel hat die Klasse CheckoutService eine direkte Abhängigkeit zur Klasse PaymentService.

public class CheckoutService
{
    private PaymentService paymentService = new PaymentService();

    public void DoCheckout() {
       // …. 
       paymentService.PayByCreditCard(cardNo, owner, pin, amount, description);
    }
}

Beim Ausführen der Methode DoCheckout wird der PaymentService aufgerufen und eine Zahlung durchgeführt. Für das automatisierte Testen ist dies ungeeignet, weil dann mit jedem Testdurchlauf mit dem Zahlungsdienstleister kommuniziert würde. Durch spezielle Testdaten wäre das möglicherweise noch praktikabel, hätte aber einen sehr negativen Einfluss auf die Laufzeit und Stabilität der Tests. Daher führe ich im ersten Schritt ein Interface für die Klasse PaymentService ein. Das Ergebnis von Extract Interface:

public interface IPaymentService
{
    void PayByCreditCard(string cardNo, string owner, string pin, double amount, string decription);
}

Nun muss in der Klasse CheckoutService, die den PaymentService verwendet, eine Möglichkeit geschaffen werden, in Tests eine Attrappe zu verwenden. Dazu stellen ich die Verwendung auf das Interface statt dem konkreten Typ um. Ferner ergänze ich zwei Konstruktoren. Der öffentliche Defaultkonstruktor leitet seine Initialisierung an den internen „echten“ Konstruktor weiter. Dabei wird eine Instanz des PaymentService erzeugt, so wie es zuvor bei der Feldinitialisierung bereits der Fall war:

private IPaymentService paymentService;

public CheckoutService() : this(new PaymentService()) {
}

internal CheckoutService(IPaymentService paymentService) {
    this.paymentService = paymentService;
}

Das Einführen der zusätzlichen Konstruktoren entspricht der Vorgehensweise im vorigen Abschnitt, in dem wir einen DateTime.Now Aufruf durch eine Func ersetzt hatten. Neu ist hier die Einführung des Interface mittels Extract Interface.

Fazit

 

Benutzen Sie Extract Interface, um zu einer vorhandenen Klasse ein Interface zu generieren. Durch Verwendung des Interface anstelle des konkreten Typs kann die Implementation austauschbar gemacht werden. Dies ist für das automatisierte Testen hilfreich.

 

Unsere Seminare

course
Clean Code Developer Basics

Prinzipien und Tests – Das Seminar wendet sich an Softwareentwickler, die gerade beginnen, sich mit dem Thema Softwarequalität auseinanderzusetzen. Es werden die wichtigsten Prinzipien und Praktiken der Clean Code Developer Initiative vermittelt.

zum Seminar »
course
Clean Code Developer Advanced

Mit Flow Design von den Anforderungen zum Clean Code – Lernen Sie mit Flow Design einen Softwareentwicklungsprozess kennen, der Sie flüssig von den Anforderungen zum Clean Code führt.

zum Seminar »
course
Clean Code Developer Trainer

Seminare als Trainer durchführen – Dieses Seminar wendet sich an Softwareentwickler, die ihr Wissen über die Clean Code Developer Prinzipien und Praktiken bzw. über Flow Design als Trainer an andere weitergeben möchten.

zum Seminar »
course
Clean Code Developer CoWorking

Online CoWorking inkl. Coaching –
Wir werden häufig gefragt, was man als Entwickler tun könne, um kontinuierlich dran zu bleiben am Thema Clean Code Developer. Unsere Antwort: Treffen Sie sich regelmäßig wöchentlich online mit anderen Clean Code Developern.

zum Seminar »

Kommentar verfassen

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

de_DEGerman