[Objective–C|Testing] Message Sending Observation?

Diese Seite verwendet Cookies. Durch die Nutzung unserer Seite erklären Sie sich damit einverstanden, dass wir Cookies setzen. Weitere Informationen

  • [Objective–C|Testing] Message Sending Observation?

    Moin!

    Nachdem ja objektorientierte Programmierung in der Hauptsache aus Objekten und Nachrichten besteht, und ich kein Freund von so unsäglich komplizierten Mocking Objekten bin, dachte ich mir:
    Gibts da nicht was von Objective-C?

    Ich meine, ich kann Schlüssel observieren, mich über allen möglichen und unmöglichen Scheiß benachrichtigen lassen, herausbekommen ob auf eine Nachricht reagiert werden kann, direkt eine Nachricht absenden – ich kann mir sogar die Implementierung dessen ausgeben lassen, was auf die Nachricht geschehen wird.

    Da liegt es doch nahe sich darüber informieren zu lassen, wenn eine Nachricht versendet wurde.

    Beispiel:
    In einem Integrationstest möchte ich feststellen, ob beim Erstellen eines neuen Benutzers auch ein Bankkonto angelegt wird.
    Nun reagiert die Erstellung des Bankkontos aber noch nicht sinnvoll sondern gibt nil zurück.
    Ich will in diesem Kontext überhaupt nicht wissen, ob das Anlegen eines Bankkontos erfolgreich war. Dementsprechend möchte ich auch nicht prüfen, ob irgend eine interne Variable gesetzt wurde oder nicht. (Key–Value–Observing fällt also aus)
    Mich interessiert nur, ob das Instanzobjekt 'Egon' dem Klassenobjekt 'BankAccount' eine Nachricht 'accountWithUser:' geschickt hat.
    (Oder alternativ, ob das Instanzobjekt 'Egon' dem Instanzobjekt 'egonsAccount' der Klasse 'BankAccount' eine Nachricht 'initWithUser' geschickt hat.)

    Ja, ich weiß, dass man Mocking Objekte so einrichten kann, dass die fröhlich auf alles horchen und brav antworten, wenn sowas passiert ist.
    Ich möchte aber wissen, ob das mit reinen Objective–C Bordmitteln geht.

    Wer weiß was? :)
    «Applejack» "Don't you use your fancy mathematics to muddle the issue!"

    Iä-86! Iä-64! Awavauatsh fthagn!

    kmr schrieb:

    Ach, Du bist auch so ein leichtgläubiger Zeitgenosse, der alles glaubt, was irgendwelche Typen vor sich hin brabbeln. :-P
  • [Markus Müller]
    Ich verstehe die Antwort nicht.
    Meine Implementierung in User kennt doch diese Subklasse gar nicht und kann auch nicht mit ihr arbeiten.

    [macmoonshine]
    Den schau ich mir gern noch mal an.
    «Applejack» "Don't you use your fancy mathematics to muddle the issue!"

    Iä-86! Iä-64! Awavauatsh fthagn!

    kmr schrieb:

    Ach, Du bist auch so ein leichtgläubiger Zeitgenosse, der alles glaubt, was irgendwelche Typen vor sich hin brabbeln. :-P
  • Marco Feltmann schrieb:

    In einem Integrationstest möchte ich feststellen, ob beim Erstellen eines neuen Benutzers auch ein Bankkonto angelegt wird.
    Nun reagiert die Erstellung des Bankkontos aber noch nicht sinnvoll sondern gibt nil zurück.
    Ich will in diesem Kontext überhaupt nicht wissen, ob das Anlegen eines Bankkontos erfolgreich war. Dementsprechend möchte ich auch nicht prüfen, ob irgend eine interne Variable gesetzt wurde oder nicht. (Key–Value–Observing fällt also aus)
    Mich interessiert nur, ob das Instanzobjekt 'Egon' dem Klassenobjekt 'BankAccount' eine Nachricht 'accountWithUser:' geschickt hat.
    (Oder alternativ, ob das Instanzobjekt 'Egon' dem Instanzobjekt 'egonsAccount' der Klasse 'BankAccount' eine Nachricht 'initWithUser' geschickt hat.)
    Du willst also etwas Unfertiges Testen. Das ist nicht sinnvoll.
    Es hat noch nie etwas gefunzt. To tear down the Wall would be a Werror!
    25.06.2016: [Swift] gehört zu meinen *Favorite Tags* auf SO. In welcher Bedeutung von "favorite"?
  • Du könntest du Klasse BankAccount so anpassen, dass deine eigene Implementierung ausgeführt wird.
    1. Implementierung von accountWith: ändern, sodass du informiert wirst (z.B. das setzten irgendeiner Instanzvariable)
    2. Test durchlaufen
    3. Die alte Implementierung von accountWith: wieder setzen, damit ggf. andere Tests danach noch funktionieren
    4. "(z.B. das setzten irgendeiner Instanzvariable)" prüfen ob das passiert ist
    Sowas ähnliches habe ich mal gebraucht, als ich die Integration von Singletons testen musste und die eigentliche Singleton-Instanz durch ein Mock-Objekt ausgetauscht habe. An dieser Stelle: Benutzt keine Singletons, wenn es sich vermeiden lässt.

    Noch ein Tipp: developer.apple.com/library/ma…Reference/ObjCRuntimeRef/
  • Warum so kompliziert? Das ganze hört sich für meine Ohren auch nicht nach einem Integrationstest an, sondern nach einem UnitTest (Du sagst ja selber, dass der Bankaccount noch nicht fertig ist, sollte er für einen Integrationstest aber sein):

    Quellcode

    1. - (void)testThatCreatingAUserInvokesNewAccountOnBank {
    2. BankAccountSpy *spy = [BankAccountSpy new];
    3. SystemUnderTest *sut = [[SystemUnderTest alloc] initWithBankAccount:spy];
    4. [sut createUserWithName:@"Username"];
    5. XCTAssertTrue(spy.accountWithUserInvoked);
    6. XCTAssertEqualObjects(spy.userName, @"Username");
    7. }


    BankAccountSpy überschreibt die entspr BankAccount Methoden und hat zusätzlich die im Test verwendeten getter. Da brauchst Du kein AOP oder irgendwelche obskure Obj-C runtime magic für. Und das schönste: es klappt auch mit Swift SCNR ;)

    Beste Grüße, Markus
  • Amin
    Doch, etwas Unfertiges testen zu wollen ist sehr wohl sinnvoll.
    Ich teste nämlich nicht auf Ergebnisse. Meinen frisch erstellten User interessiert es nämlich überhaupt nicht, ob sein BankAccount erstellt wurde oder nicht.
    Meinen frisch erstellten User interessiert nur, dass er die Erstellung eines BankAccount angestoßen und sich selbst als Besitzer eingetragen hat.
    Ob der Kram nachher $irgendwas macht, ist dem User egal. Dafür gibt's den BankAccount Unit Test.

    Im Übrigen will ich nicht alle Methoden überwachen. Ich möchte [bankAccountInstance init]; und [bankAccountInstance setUser:]; überwachen.

    So, wie ich beim Key–Value–Observing keinesfalls die Werte aller Keys observieren will. Nur die Werte einzelner Keys.

    Insofern werde ich mir wohl eine Category basteln, die ähnlich dem KVO einen Observer (am Besten einen Block) für die Methoden einer Klasse bastelt.

    Markus
    Du bastelst Dir hier Objekte, die eine Funktionalität abbilden, die das System an sich bietet.
    Natürlich klappt das auch mit Swift. Du schleppst Dir halt nur Unmengen redundanter und damit überflüssiger Objekte mit.

    All diese überflüssigen Objekte existieren doch nur, weil Java, C++, C#, PHP und wie sie alle heißen einen ganz wichtigen Aspekt der objektorientierten Programmierung ignorieren:
    "It's all about the message."

    Via XCTAssertTrue(spy.accountWithUserInvoked); prüfst Du ein Ergebnis. Die Testsuites prüfen überall nur auf ein Ergebnis.
    Wenn es eigentlich kein Ergebnis geben kann ('Nachricht wurde gesendet.') wird ein Ergebnis drum rum gefaked. Das ist meiner Auffassung nach am Prinzip vorbei.
    Und deshalb bevorzuge ich irgendwelche obskure Obj–C Runtime Magic.

    Zumal ich überhaupt nicht wissen kann und will, in welcher Art und Weise die Nachrichten wo aufgerufen werden.
    Vielleicht ist mein BankAccount ja ein Class Cluster. Vielleicht ist mein BankAccount auch nur ein Protokoll. Vielleicht habe ich spezielle Subklassen, die –initWithUser: ihrer Elternklasse aufrufen, vielleicht rufen sie auch nur –init und –setUser: ihrer Elternklasse auf.
    Der User (und ich) weiß zum Zeitpunkt des Testens nicht, wie BankAccount implementiert ist. (Vielleicht ist es ja noch gar nicht implementiert und init liefert nil.)
    Das Basteln eines Spy Objektes legt aber ganz genau fest, wie das Ding da aussehen soll.
    Im Unit Test für User habe ich also eine dicke fette Abhängigkeit irgend eines definierten BankAccount Objekts, welches den User überhaupt nicht interessiert.
    Auch wenn ich hier mocke, dass mich nur der Aufruf von –accountWithUser: interessiert, verrate ich ziemlich viel über die Implementierung, über die ich noch nichts wissen kann.
    (Hey, TDD. Ich habe noch keine Ahnung wie mein BankAccount aussehen wird, ich häng ja noch am User und weiß nur, dass der mitgeneriert werden soll.)

    Wenn ich später entschließe, dass ich lieber –accountWithType:forUser: aufrufen möchte, ist mein Test hinfällig – obwohl sich an der Funktionalität nichts geändert hat.

    Kurzum: Ich möchte gezielt auf einzelne Nachrichten testen, nicht auf fest definierte Methodenergebnisse.

    jopjip
    Ja, könnte ich. Nur ruft niemand irgendwo eigene Klassen auf.
    Und Key–Value–Observing läuft auch automatisch, ohne dass ich da selbst irgendwie an den Settern rumschrauben muss.
    Warum sollte ich also irgend etwas anderes machen als es das System an sich tut?
    «Applejack» "Don't you use your fancy mathematics to muddle the issue!"

    Iä-86! Iä-64! Awavauatsh fthagn!

    kmr schrieb:

    Ach, Du bist auch so ein leichtgläubiger Zeitgenosse, der alles glaubt, was irgendwelche Typen vor sich hin brabbeln. :-P
  • Also irgendwie verstehe ich Dein Problem nicht. Du willst Interaktionen zwischen Objekten testen, das hat mit der verwendeten Sprache nichts zu tun.

    Wenn Du testen möchtest, ob User eine bestimmte Methode eines anderen Objektes aufrufst, hast Du da eine Abhängigkeit. Design by contract, wie ich schon oben schrieb: plain old OO stuff.

    Der von mir vorgeschlagene Test bildet genau das ab und ist in keiner Weise an irgendwelche Implementationsdetails gekoppelt, BankAccount kann (und sollte) ein Interface sein, dann implementiert der Spy ebenjenes.

    Aber mach wie Du denkst. Ich persönlich finde ein einfaches Testobjekt viel leichter zu verstehen (6Monate später), als runtime magic oder irgendwelche mockingframeworks, die mit der nächsten Xcode-Version kaputt gehen.

    Und zu TDD: ich sehe da keinen Widerspruch zu meiner vorgeschlagen Lösung - genau so etwas ist TDD. Du bekommst Klarheit über etwaige Abhängigkeiten.
  • Markus Müller schrieb:

    Also irgendwie verstehe ich Dein Problem nicht. Du willst Interaktionen zwischen Objekten testen, das hat mit der verwendeten Sprache nichts zu tun.

    Wenn Du testen möchtest, ob User eine bestimmte Methode eines anderen Objektes aufrufst, hast Du da eine Abhängigkeit. Design by contract, wie ich schon oben schrieb: plain old OO stuff.

    Der von mir vorgeschlagene Test bildet genau das ab und ist in keiner Weise an irgendwelche Implementationsdetails gekoppelt, BankAccount kann (und sollte) ein Interface sein, dann implementiert der Spy ebenjenes.

    Aber mach wie Du denkst. Ich persönlich finde ein einfaches Testobjekt viel leichter zu verstehen (6Monate später), als runtime magic oder irgendwelche mockingframeworks, die mit der nächsten Xcode-Version kaputt gehen.

    Und zu TDD: ich sehe da keinen Widerspruch zu meiner vorgeschlagen Lösung - genau so etwas ist TDD. Du bekommst Klarheit über etwaige Abhängigkeiten.
    Sein Plan hat mit TDD sogar überhaupt nichts zu tun. Bei TDD schreibe ich einen Test, der gerade genau einer Spezifikation entspricht, gehe davon aus, dass er zunächst fehlschlägt und implementiere dann so, dass er nicht mehr fehlschlägt.

    Er will einen fehlerhaften Test unterschlagen.
    Es hat noch nie etwas gefunzt. To tear down the Wall would be a Werror!
    25.06.2016: [Swift] gehört zu meinen *Favorite Tags* auf SO. In welcher Bedeutung von "favorite"?