Einfache Refactorings – Teil 5

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<DateTime> 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<DateTime> 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.

4 Gedanken zu „Einfache Refactorings – Teil 5“

  1. Die Implementation für die Tests zu überschreiben geht natürlich auch, ist allerdings mehr Aufwand. Den möchte ich ungern treiben, wenn es doch mit Func auch geht. Es beginnt da ja schon mit der Frage, wo du die für den Test überladene Klasse ablegst.
    Ferner signalisiert die protected Property einem Verwender der Klasse, dass eine Überschreibung der Klasse möglich oder sogar erwünscht bis erforderlich ist. Das setzt mir das falsche Signal. Wenn es technisch mal nicht anders geht, fein. Aber im konkreten Beispiel geht’s ja wunderbar mit der Func.

    Antworten
    • Die überladene Klasse gehört in das Testprojekt und als internal markiert, da sie im produktiven Code nichts zu suchen hat. Richtig, das ist unschön. Das gleiche Problem hast du aber auch beim internen Konstruktor, sofern die Klasse im selben Projekt verwendet wird. In diesem Fall ist die Hürde sogar noch kleiner. Aber ich stimme mit dir überein, im Beispiel oben ist die Lösung mit Func eleganter 🙂

      Antworten

Schreibe einen Kommentar