Ducks, Mocks and Rock’n Roll
Angeregt durch David Meyer’s Implementierung einer Duck Typing Klasse und diesen Beitrag von Phil Haack möchte ich im folgenden auf die Möglichkeiten eingehen die Duck Typing in Verbindung mit Mocks beim Testen bietet.
Wenn man Software Entwicklung testgetrieben betreibt ergeben sich an vielen Stellen von ganz alleine Interfaces die später durch eine konkrete Klasse implementiert werden. Beginnt man dagegen zunächst mit der Implementierung der konkreten Klassen erscheinen Interfaces zunächst etwas unnatürlich da man sie möglicherweise ausschließlich fürs Testen einführt. Zwar ist es auch ohne Interfaces möglich bei der Implementierung Dependency Injection zu verwenden, aber spätestens beim Testen merkt man, dass die injizierten Objekte nur schwer durch Mocks oder Stubs ersetzt werden können.
Dazu ein Beispiel. Nehmen wir an eine Klasse Auftrag benötigt eine Methode zur Berechnung des Preises. Nehmen wir weiters an die Preisberechnung wäre recht aufwendig, dann macht es Sinn diese in einer eigenen Klasse zu implementieren (single responsibility principle). Dies könnte man wie folgt realisieren:
namespace DucksAndMocksAndRockNRoll { public class Auftrag { private Preisberechnung preisberechnung; private float einzelpreis; private float menge; public Auftrag(Preisberechnung preisberechnung) { this.preisberechnung = preisberechnung; } public float Einzelpreis { get { return einzelpreis; } set { einzelpreis = value; } } public float Menge { get { return menge; } set { menge = value; } } public float Preis() { return preisberechnung.Preis(this); } } public class Preisberechnung { public float Preis(Auftrag auftrag) { return auftrag.Menge * auftrag.Einzelpreis; } } }
Durch Testen der Klasse Auftrag möchte ich sicherstellen, dass in der Methode Preis das injizierte Objekt preisberechnung korrekt aufgerufen wird. Dazu instanziiere ich ein Objekt der zu testenden Klasse Auftrag in der Regel so, dass Abhängigkeiten durch ein Stub oder Mock ersetzt werden. Andernfalls würde ich nicht nur die Klasse die getestet werden soll testen sondern implizit auch die injizierten Abhängigkeiten.
Da die Dependency Injection in meinem Beispiel nicht über ein Interface sondern eine konkrete Klasse erfolgt habe ich nur die Möglichkeit, von der Klasse Preisberechnung abzuleiten. Wenn die Klasse dazu noch sealed wäre (was bei Framework Klassen oft der Fall ist) habe ich nicht einmal diese Möglichkeit.
An diesem Punkt werde ich demnach die Abhängigkeit der Klasse Auftrag von der Klasse Preisberechnung über ein dazwischen liegendes Interface entkoppeln. Neben der Tatsache dass damit die Testbarkeit der Klasse erhöht wird entsteht dadurch auch die Möglichkeit unterschiedliche Implementierungen der Preisberechnung zur Verfügung zu stellen.
namespace DucksAndMocksAndRockNRoll { public class Auftrag { private IPreisberechnung preisberechnung; private float einzelpreis; private float menge; public Auftrag(IPreisberechnung preisberechnung) { this.preisberechnung = preisberechnung; } public float Einzelpreis { get { return einzelpreis; } set { einzelpreis = value; } } public float Menge { get { return menge; } set { menge = value; } } public float Preis() { return preisberechnung.Preis(this); } } public interface IPreisberechnung { float Preis(Auftrag auftrag); } public class Preisberechnung : IPreisberechnung { public float Preis(Auftrag auftrag) { return auftrag.Menge * auftrag.Einzelpreis; } } }
Nun kann ich im Test die Klasse Auftrag instanziieren und ein Mock oder Stub Objekt im Konstruktor übergeben.
[TestFixture] public class AuftragTests { private MockRepository mocks; private IPreisberechnung preisberechnung; private Auftrag auftrag; [SetUp] public void Setup() { mocks = new MockRepository(); preisberechnung = mocks.DynamicMock<IPreisberechnung>(); auftrag = new Auftrag(preisberechnung); } [Test] public void PreisRuftPreisberechnungAuf() { using (mocks.Record()) { Expect.Call(preisberechnung.Preis(auftrag)).Return(5.90f); } using (mocks.Playback()) { Assert.That(auftrag.Preis(), Is.EqualTo(5.90f)); } } }
Voraussetzung für diese Vorgehensweise ist allerdings, dass ich das Interface in der Implementierung ergänzen kann. Wenn ich nicht in der Lage bin die Klasse Preisberechnung um das Interface IPreisberechnung zu erweitern, wie es z.B. bei Framework Klassen der Fall ist, bleibt mir die Möglichkeit einen Adapter einzuführen der IPreisberechnung implementiert:
public interface IPreisberechnung { float Preis(Auftrag auftrag); } public sealed class Preisberechnung { public float Preis(Auftrag auftrag) { return auftrag.Menge * auftrag.Einzelpreis; } } public class PreisberechnungAdapter : IPreisberechnung { private readonly Preisberechnung preisberechnung; public PreisberechnungAdapter(Preisberechnung preisberechnung) { this.preisberechnung = preisberechnung; } public float Preis(Auftrag auftrag) { return preisberechnung.Preis(auftrag); } }
Der Adapter übernimmt in seinem Konstruktor ein Objekt vom Typ Preisberechnung und implementiert das Interface IPreisberechnung, in dem er alle Aufrufe an die übergebene Instanz der Preisberechnung weiterleitet. Dadurch kann ich in meiner abhängigen Klasse Auftrag das Interface verwenden und dennoch die abgeschlossene Klasse Preisberechnung verwenden.
Der zu implementierende Adapter ist vom Aufbau her einfach und fällt in die Kategorie Fleißarbeit. Um sicher zu stellen dass er korrekt implementiert ist sollten dazu Unit Tests angefertigt werden. Dabei landen wir jedoch wieder am Ausgangspunkt, nämlich bei einer Klasse die eine Abhängigkeit zu einer konkreten Klasse hat die nicht durch einen Mock ersetzt werden kann.
Wenn man nun einen Mechanismus hätte mit dem man den Adapter automatisch erzeugt und durch Tests sicherstellen könnte dass das verwendete Verfahren zu korrekt implementierten Adaptern führt hätte man zwei Probleme auf einmal gelöst. Und an dieser Stelle kommt (endlich) Duck Typing ins Spiel:
public void Initialize() { preisberechnung = DuckTyping.Cast<IPreisberechnung>(new Preisberechnung()); auftrag = new Auftrag(preisberechnung); }
Beim Testen bleibt alles wie oben, durch das Interface bin ich in der Lage Mocks zu verwenden. Im Echtbetrieb injiziere ich statt eines Adapter Objektes ein Duck Objekt. Dazu wird zur Laufzeit eine Klasse generiert die das benötigte Interface IPreisberechnung implementiert. Ferner wird eine konkrete Instanz der zu adaptierenden Klasse an den Duck übergeben. Die generierte Klasse verhält sich wie ein Adapter, sie leitet die Aufrufe an das übergebene Objekt weiter.
Somit bietet Duck Typing die Möglichkeit, bei der Implementierung eigene Interfaces zu verwenden um dadurch die Testbarkeit sicherzustellen. Das Erstellen von Adaptern “zu Fuß” entfällt, da diese zur Laufzeit generiert werden.