Ich habe die sehr lange und mühsame Suche begonnen, TDD zu lernen und auf meinen Workflow anzuwenden . Ich habe den Eindruck, dass TDD sehr gut zu den IoC-Prinzipien passt.
Nachdem ich einige Fragen mit TDD-Tags hier in SO durchsucht habe, habe ich gelesen, dass es eine gute Idee ist, gegen Schnittstellen und nicht gegen Objekte zu programmieren.
Können Sie einfache Codebeispiele dafür bereitstellen, wie dies ist und wie es in realen Anwendungsfällen angewendet werden kann? Einfache Beispiele sind für mich (und andere Leute, die lernen wollen) der Schlüssel, um die Konzepte zu verstehen.
c#
.net
tdd
inversion-of-control
ROMANIA_engineer
quelle
quelle
interface
Java noch mit C # zu tun . Als das Buch, aus dem dieses Zitat stammt, geschrieben wurde, existierten weder Java noch C #.List
, dass sich dasadd
Element nach dem Hinzufügen eines Elements zur Liste in der Liste befindet und die Länge der Liste um zunimmt1
. Wo steht das eigentlich im [interface List
] ( Download.Oracle.Com/javase/7/docs/api/java/util/List.html#add )?Antworten:
Erwägen:
class MyClass { //Implementation public void Foo() {} } class SomethingYouWantToTest { public bool MyMethod(MyClass c) { //Code you want to test c.Foo(); } }
Da
MyMethod
nur a akzeptiert wirdMyClass
, können Sie nicht durchMyClass
ein Scheinobjekt ersetzen, um einen Unit-Test durchzuführen. Besser ist es, eine Schnittstelle zu verwenden:interface IMyClass { void Foo(); } class MyClass : IMyClass { //Implementation public void Foo() {} } class SomethingYouWantToTest { public bool MyMethod(IMyClass c) { //Code you want to test c.Foo(); } }
Jetzt können Sie testen
MyMethod
, da nur eine Schnittstelle und keine bestimmte konkrete Implementierung verwendet wird. Dann können Sie diese Schnittstelle implementieren, um jede Art von Mock oder Fake zu erstellen, die Sie zu Testzwecken benötigen. Es gibt sogar Bibliotheken wie die von Rhino MocksRhino.Mocks.MockRepository.StrictMock<T>()
, die jede Schnittstelle verwenden und Ihnen im Handumdrehen ein Scheinobjekt erstellen.quelle
interface
Schlüsselwörtern im gesamten Code nicht, dass die Programmierung gegen Schnittstellen erfolgt . Gedankenexperiment: Nehmen Sie schrecklich eng gekoppelten inkohäsiven Code. Kopieren Sie sie für jede Klasse einfach und fügen Sie sie ein, löschen Sie alle Methodenkörper, ersetzen Sie dasclass
Schlüsselwort durchinterface
und aktualisieren Sie alle Verweise im Code auf diesen Typ. Ist der Code jetzt besser?Es ist alles eine Frage der Intimität. Wenn Sie eine Implementierung (ein realisiertes Objekt) codieren, stehen Sie als Verbraucher in einer ziemlich engen Beziehung zu diesem "anderen" Code. Es bedeutet, dass Sie wissen müssen, wie es zu konstruieren ist (dh welche Abhängigkeiten es hat, möglicherweise als Konstruktorparameter, möglicherweise als Setter), wann Sie es entsorgen müssen, und Sie können wahrscheinlich nicht viel ohne es tun.
Über eine Schnittstelle vor dem realisierten Objekt können Sie einige Dinge tun -
** UPDATE ** Es wurde ein Beispiel für einen IOC-Container (Factory) angefordert. Es gibt viele für so ziemlich alle Plattformen, aber im Kern funktionieren sie so:
Sie initialisieren den Container in Ihrer Anwendungsstartroutine. Einige Frameworks tun dies über Konfigurationsdateien oder Code oder beides.
Sie "registrieren" die Implementierungen, die der Container für Sie als Factory für die von ihnen implementierten Schnittstellen erstellen soll (z. B.: Registrieren Sie MyServiceImpl für die Service-Schnittstelle). Während dieses Registrierungsprozesses können Sie normalerweise einige Verhaltensrichtlinien angeben, z. B. wenn jedes Mal eine neue Instanz erstellt wird oder eine einzelne (Tonne) Instanz verwendet wird
Wenn der Container Objekte für Sie erstellt, fügt er im Rahmen des Erstellungsprozesses alle Abhängigkeiten in diese Objekte ein (dh wenn Ihr Objekt von einer anderen Schnittstelle abhängt, wird wiederum eine Implementierung dieser Schnittstelle bereitgestellt usw.).
Pseudocodisch könnte es so aussehen:
IocContainer container = new IocContainer(); //Register my impl for the Service Interface, with a Singleton policy container.RegisterType(Service, ServiceImpl, LifecyclePolicy.SINGLETON); //Use the container as a factory Service myService = container.Resolve<Service>(); //Blissfully unaware of the implementation, call the service method. myService.DoGoodWork();
quelle
Wenn Sie für eine Schnittstelle programmieren, schreiben Sie Code, der eine Instanz einer Schnittstelle verwendet, keinen konkreten Typ. Beispielsweise könnten Sie das folgende Muster verwenden, das die Konstruktorinjektion enthält. Konstruktorinjektion und andere Teile der Umkehrung der Steuerung sind nicht erforderlich, um gegen Schnittstellen programmieren zu können. Da Sie jedoch aus der TDD- und IoC-Perspektive kommen, habe ich sie auf diese Weise verkabelt, um Ihnen einen Kontext zu geben, den Sie hoffentlich haben vertraut mit.
public class PersonService { private readonly IPersonRepository repository; public PersonService(IPersonRepository repository) { this.repository = repository; } public IList<Person> PeopleOverEighteen { get { return (from e in repository.Entities where e.Age > 18 select e).ToList(); } } }
Das Repository-Objekt wird übergeben und ist ein Schnittstellentyp. Der Vorteil der Übergabe einer Schnittstelle besteht in der Möglichkeit, die konkrete Implementierung auszutauschen, ohne die Verwendung zu ändern.
Zum Beispiel würde man annehmen, dass der IoC-Container zur Laufzeit ein Repository injiziert, das so verdrahtet ist, dass es die Datenbank erreicht. Während der Testzeit können Sie ein Mock- oder Stub-Repository übergeben, um Ihre
PeopleOverEighteen
Methode auszuführen.quelle
Es bedeutet, generisch zu denken. Unspezifisch.
Angenommen, Sie haben eine Anwendung, die den Benutzer benachrichtigt, der ihm eine Nachricht sendet. Wenn Sie beispielsweise mit einer Schnittstellen-IMessage arbeiten
interface IMessage { public void Send(); }
Sie können pro Benutzer die Art und Weise anpassen, in der er die Nachricht empfängt. Zum Beispiel möchte jemand mit einer E-Mail benachrichtigt werden, und Ihr IoC erstellt eine konkrete EmailMessage-Klasse. Einige andere möchten SMS, und Sie erstellen eine Instanz von SMSMessage.
In all diesen Fällen wird der Code zur Benachrichtigung des Benutzers niemals geändert. Auch wenn Sie eine weitere konkrete Klasse hinzufügen.
quelle
Der große Vorteil der Programmierung gegen Schnittstellen bei Unit-Tests besteht darin, dass Sie einen Code von allen Abhängigkeiten isolieren können, die Sie separat testen oder während des Tests simulieren möchten.
Ein Beispiel, das ich hier bereits erwähnt habe, ist die Verwendung einer Schnittstelle für den Zugriff auf Konfigurationswerte. Anstatt ConfigurationManager direkt zu betrachten, können Sie eine oder mehrere Schnittstellen bereitstellen, über die Sie auf Konfigurationswerte zugreifen können. Normalerweise würden Sie eine Implementierung bereitstellen, die aus der Konfigurationsdatei liest, aber zum Testen können Sie eine verwenden, die nur Testwerte zurückgibt oder Ausnahmen oder was auch immer auslöst.
Berücksichtigen Sie auch Ihre Datenzugriffsschicht. Wenn Ihre Geschäftslogik eng an eine bestimmte Datenzugriffsimplementierung gekoppelt ist, ist das Testen schwierig, ohne dass eine gesamte Datenbank mit den benötigten Daten zur Verfügung steht. Wenn Ihr Datenzugriff hinter Schnittstellen verborgen ist, können Sie nur die Daten angeben, die Sie für den Test benötigen.
Die Verwendung von Schnittstellen vergrößert die zum Testen verfügbare "Oberfläche" und ermöglicht feinkörnigere Tests, mit denen einzelne Einheiten Ihres Codes wirklich getestet werden.
quelle
Testen Sie Ihren Code wie jemanden, der ihn nach dem Lesen der Dokumentation verwenden würde. Testen Sie nichts basierend auf Ihrem Wissen, da Sie den Code geschrieben oder gelesen haben. Sie möchten sicherstellen, dass sich Ihr Code wie erwartet verhält .
Im besten Fall sollten Sie Ihre Tests als Beispiele verwenden können. Doctests in Python sind hierfür ein gutes Beispiel.
Wenn Sie diese Richtlinien befolgen, sollte das Ändern der Implementierung kein Problem sein.
Auch nach meiner Erfahrung ist es empfehlenswert, jede "Schicht" Ihrer Anwendung zu testen. Sie haben atomare Einheiten, die an sich keine Abhängigkeiten haben, und Sie haben Einheiten, die von anderen Einheiten abhängen, bis Sie schließlich zu der Anwendung gelangen, die an sich eine Einheit ist.
Sie sollten jede Schicht testen und sich nicht darauf verlassen, dass Sie durch Testen von Einheit A auch Einheit B testen, von der Einheit A abhängt (die Regel gilt auch für die Vererbung). Auch dies sollte als Implementierungsdetail behandelt werden obwohl Sie vielleicht das Gefühl haben, sich zu wiederholen.
Beachten Sie, dass sich geschriebene Tests wahrscheinlich nicht ändern, während sich der zu testende Code fast definitiv ändert.
In der Praxis gibt es auch das Problem von E / A und der Außenwelt. Daher möchten Sie Schnittstellen verwenden, damit Sie bei Bedarf Mocks erstellen können.
In dynamischeren Sprachen ist dies kein so großes Problem. Hier können Sie Ententypisierung, Mehrfachvererbung und Mixins verwenden, um Testfälle zu erstellen. Wenn Sie anfangen, Vererbung im Allgemeinen nicht zu mögen, machen Sie es wahrscheinlich richtig.
quelle
Dieser Screencast erklärt die agile Entwicklung und TDD in der Praxis für c #.
Wenn Sie gegen eine Schnittstelle codieren, können Sie in Ihrem Test ein Scheinobjekt anstelle des realen Objekts verwenden. Mit einem guten Mock-Framework können Sie in Ihrem Mock-Objekt alles tun, was Sie möchten.
quelle