thomas.dohmke.de - tagged with objective-c http://thomas.dohmke.de/feed en-us http://blogs.law.harvard.edu/tech/rss Sweetcron thomas@dohmke.de Kurztip: Fehlermeldung "Unrecognized Selector" in Xcode http://thomas.dohmke.de/items/view/265/kurztip-fehlermeldung-quotunrecognized-selectorquot-in-xcode

Von Zeit zu Zeit wird ein Fehler während des Programmierens in Xcode mit der folgenden Fehlermeldung beim Ablauf des Programms bestraft:

2009-03-18 18:51:12.734 PlayGround[51029:20b] *** -[NSCFString someString]: unrecognized selector sent to instance 0xa05b1328

Der Stacktrace des Debuggers beinhaltet dabei nicht die Stelle, an der der Fehler verursacht wird. Abhilfe schafft folgende Zeile in der Datei ~/.gdbinit:

fb objc_exception_throw

Der Befehl definiert einen Breakpoint für die Funktion objc_exception_throw, die bei jeder ausgelösten Exception aufgerufen wird. Dadurch bleibt der Debugger an einer Stelle stehen, an der der Verursacher noch im Stacktrace zu finden ist.

]]>
Wed, 18 Mar 2009 19:02:00 +0100 http://thomas.dohmke.de/items/view/265/kurztip-fehlermeldung-quotunrecognized-selectorquot-in-xcode
iPhone Code Schnipsel: Subviews umdrehen http://thomas.dohmke.de/items/view/221/iphone-code-schnipsel-subviews-umdrehen

Gegeben¶

Ein Fenster mit zwei Subviews:

[window addSubview:view1]; [window addSubview:view2];

Gesucht¶

Zwischen den Views wechseln und dabei eine Animation anzeigen, die ein Umdrehen symbolisiert.

Lösung¶

CGContextRef context = UIGraphicsGetCurrentContext(); [UIView beginAnimations:nil context:context]; [UIView setAnimationTransition:UIViewAnimationTransitionFlipFromLeft forView:window cache:YES]; [UIView setAnimationCurve:UIViewAnimationCurveEaseInOut]; [UIView setAnimationDuration:1.0]; [window exchangeSubviewAtIndex:1 withSubviewAtIndex:0]; [UIView commitAnimations];

Will man von rechts nach links drehen, nimmt man UIViewAnimationTransitionFlipFromRight statt dem obigen UIViewAnimationTransitionFlipFromLeft.

]]>
Thu, 05 Mar 2009 15:06:00 +0100 http://thomas.dohmke.de/items/view/221/iphone-code-schnipsel-subviews-umdrehen
Unit-Testing in Objective-C, Teil 3 http://thomas.dohmke.de/items/view/199/unit-testing-in-objective-c-teil-3

Im dritten Teil unserer Serie über Unit-Testing in Objective-C wollen wir das Beispielprojekt aus Teil 1 fortsetzen und dabei die Erstellung von Mock-Objekten mit OCMock kennen lernen. Der Beispielcode steht wieder als Git-Repository zur Verfügung, welches per

git clone git://www.komprovisation.de/objcut1.git

lokal geklont oder auf der Projektseite durchstöbert werden kann. Zudem ist im Artikel an dedizierten Stellen die jeweilige Revision vermerkt.

Gegeben¶

Aufbauend auf der letzten Revision des ersten Teils haben wir das Modell User bereits um eine einfache Datenbankanbindung erweitert. Als Datenbank wird SQLite zusammen mit der Bibliothek PLDatabase verwendet. Letztere hat den Vorteil, dass die Aufruf an SQLite gekapselt werden und damit ein Wechsel auf ein anderes Datenbanksystem leichter möglich sein sollte. Die Klasse User verwendet die Datenbank über die Hilfsklasse UserDatabase, die im Wesentlichen die folgenden zwei Methoden zur Verfügung stellt:

  • (User *)findWithEmail:(NSString *)email { [database open];

    User *user = nil; NSObject<PLResultSet> *results; results = [database executeQuery: @"SELECT * FROM users WHERE email = ?", email]; if ([results next]) { user = [[User alloc] init]; user.email = [results stringForColumn:@"email"]; user.hashedPassword = [results stringForColumn:@"hashed_password"]; } [results close];

    [database close];

    return [user autorelease]; }

  • (BOOL)saveWithEmail:(NSString *)email hashedPassword:(NSString *)hashedPassword { [database open];

    NSObject<PLPreparedStatement> *statement; statement = [database prepareStatement: @"INSERT INTO users (email, hashed_password) VALUES (?, ?)"]; [statement bindParameters: [NSArray arrayWithObjects: email, hashedPassword, nil]]; BOOL result = [statement executeUpdate]; [statement close];

    [database close];

    return result; }

    Die Methode findWithEmail: sucht einen Benutzer anhand der E-Mail-Adresse und liefert eine Instanz des Objektes User zurück. Die Methode saveWithEmail:hashedPassword: speichert einen Benutzer mit seiner E-Mail-Adresse und dem verschlüsselten Password. Die Verschlüsselung des Passwortes ist unabhängig von der Datenbank und erfolgt daher in User mittels der Methode encrypt:.

  • (NSString *)encrypt:(NSString *)password { unsigned char hash[CC_SHA256_DIGEST_LENGTH]; CC_SHA256([password UTF8String], [password lengthOfBytesUsingEncoding:NSUTF8StringEncoding], hash);

    NSMutableString *hashedPassword; hashedPassword = [NSMutableString stringWithCapacity:CC_SHA256_DIGEST_LENGTH];

    for (int i = 0; i < CC_SHA256_DIGEST_LENGTH; ++i) { [hashedPassword appendString:[NSString stringWithFormat:@"x", hash[i]]]; }

    return hashedPassword; }

    [Revision 7d181d7d]

    Gesucht¶

    Ziel ist es nun, die Methode loginWithEmail:password:

  • (BOOL)loginWithEmail:(NSString *)login password:(NSString *)password { return ([login isEqualToString:@"thomas@dohmke.de"] && [password isEqualToString:@"foobar"]); }

    so abzuändern, dass diese eine Klassenmethode ist und eine Instanz von User zurückliefert, sofern die Authentifizierung mit E-Mail-Adresse und Passwort erfolgreich war. E-Mail und Passwort sollen dabei mit den Werten in der Datenbank verglichen werden, d.h. die Methode muss im Gegensatz zur bisherigen Fake-Version korrekt funktionieren.

    Lösung¶

    Im ersten Schritt wandeln wir die beiden Testmethoden in UserTest.m so ab, dass sie der gewünschten Signatur entsprechend. Zur Erinnerung nachfolgend zunächst die bisherige Version:

-(void)testValidLogin { STAssertEquals([user loginWithEmail:@"thomas@dohmke.de" password:@"foobar"], YES, @"should return YES"); }

-(void)testInvalidLogin { STAssertEquals([user loginWithEmail:@"thomas@dohmke.de" password:@"wrong"], NO, @"should return NO"); }

Die Methode soll in eine Klassenmethode umgewandelt werden, daher müssen wir sie statt an die Instanz user an die Klasse User senden. Außerdem soll sie eine Instanz von User zurückliefern, entsprechend sind die Aufrufe von STAssertEquals durch STAssertNotNil bzw. STAssertNil zu ersetzen:

  • (void)testValidLogin { STAssertNotNil([User loginWithEmail:@"thomas@dohmke.de" password:@"foobar"], @"Should return user object."); }

  • (void)testInvalidLogin { STAssertNil([User loginWithEmail:@"thomas@dohmke.de" password:@"wrong"], @"Should return nil."); }

    Bei der Ausführung schlagen die Tests erwartungsgemäß fehl, da die erwartete Signatur der Methode nicht mit der Implementierung übereinstimmt. Entsprechend ändern wir die Deklaration in User.h

  • (User *)loginWithEmail:(NSString *)login password:(NSString *)password;

    und passen die Definition in User.m entsprechend an:

  • (User *)loginWithEmail:(NSString *)login password:(NSString *)password { User *user = [User findWithEmail:login]; if ([[User encrypt:password] isEqualToString:user.hashedPassword]) { return user; } else { return nil; } }

    Die Implementierung sucht den Benutzer mittels der Methode findWithEmail:, verschlüsselt den Parameter password und vergleicht das Ergebnis mit dem verschlüsselten Passwort in der von findWithEmail: gefundenen Instanz. [Revision 13222fee]

    Die Tests werden nun bestanden, allerdings ergibt sich ein neues Problem: Damit die Methode den Benutzer überhaupt in der Datenbank finden kann, müssen wir diesen dort speichern. Dies erfolgt bereits in der Methode setUp des Testfalls:

  • (void)setUp { user = [User userWithEmail:@"thomas@dohmke.de" password:@"foobar"]; STAssertEquals([user save], YES, @"save failed."); }

    Da das Speichern jedoch in derselben Datenbank erfolgt, die auch vom eigentlichen Programm benutzt wird, würden wir bei einem echten System möglicherweise reale Benutzerdaten überschreiben. Das ist unschön. Zudem wird der Benutzer mit jeder Testmethode neu angelegt wird, wie wir uns leicht auf der Konsole überzeugen können:

$ sqlite3 objcut1.db SQLite version 3.4.0 Enter ".help" for instructions sqlite> select * from users; 1|thomas@dohmke.de|c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2 2|thomas@dohmke.de|c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2 3|thomas@dohmke.de|c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2 4|thomas@dohmke.de|c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2

Hier kommt nun OCMock ins Spiel.

Bessere Lösung¶

OCMock erlaubt uns, die Klasse UserDatabase durch einen Dummy zu ersetzen und in Folge zu verhindern, dass tatsächlich in die Datenbank geschrieben wird. Entsprechend wird UserDatabase selbst nicht mehr getestet, was in Ordnung geht, da wir in UserTest.m nur die Klasse User testen wollen.

Zuerst installieren wir OCMock, in dem wir es von seiner Homepage runterladen (aktuell ist derzeit Version 1.29), das Archiv entpacken und das Verzeichnis OCMock.framework nach /Library/Framework kopieren. Dann klicken wir unter Groups & Files rechts auf Frameworks > Linked Frameworks, Menüpunkt Add > Existing Frameworks, suchen das gerade kopierte Verzeichnis und bestätigen mit Add.

Als nächstes fügen wir der Testklasse in UserTest.h eine Instanzvariable für das Mock-Objekt hinzu:

@interface UserTest : SenTestCase { // ...

id databaseMock;

}

In UserTest.m importieren wir OCMock

import <OCMock/OCMock.h>

und definieren eine neue Methode, die das Mock-Objekt erstellt:

  • (void)setUpMockDatabase { BOOL yes = YES; NSValue *wrappedValue = [NSValue valueWithBytes:&yes objCType:@encode(BOOL)];

    databaseMock = [OCMockObject mockForClass:[UserDatabase class]]; [[[databaseMock stub] andReturnValue:wrappedValue] saveWithEmail:user.email hashedPassword:user.hashedPassword]; [[[databaseMock stub] andReturn:user] findWithEmail:user.email]; [User setDatabase:databaseMock]; }

    Die ersten zwei Zeilen packen den booleschen Wert YES in eine Instanz der Klasse NSValue ein. In den folgenden drei Zeilen wird das Mock-Objekt für die Klasse UserDatabase mit zwei Stub-Methoden erstellt. Die Signaturen entsprechend dabei den Deklarationen in UserDatabase.h, wobei saveWithEmail:hashedPassword: stets YES und findWithEmail: die in setUp erstellte Instanz von User zurückgeben soll. In der letzten Zeile wird das Mock-Objekt der Klasse User übergeben, die dieses in Folge für alle Datenbankzugriffe verwendet.

    Schließlich rufen wir setUpMockDatabase in der setup-Methode auf:

  • (void)setUp { user = [User userWithEmail:@"thomas@dohmke.de" password:@"foobar"]; [self setUpMockDatabase]; STAssertEquals([user save], YES, @"save failed."); }

    Fertig. Die Tests werden weiterhin bestanden, greifen aber nicht mehr auf die Datenbank zu. [Revision aeb659e1]

    Erster Nachgang¶

    Neben stub können Stub-Methoden auch mit expect definiert werden. Der Unterschied besteht darin, dass eine mit expect definierte Methode genau einmal aufgerufen werden muss, während die Anzahl der Aufrufe bei "stub" gleichgültig ist, d.h. auch kein Aufruf ist erlaubt. Will man mehr als einen Aufruf kontrollieren, ist das expect-Statement entsprechend oft zu wiederholen.

    Als Beispiel verändern wir setUpMockDatabase wie folgt

  • (void)setUpMockDatabase { // ...

    [[[databaseMock expect] andReturnValue:wrappedValue] saveWithEmail:user.email hashedPassword:user.hashedPassword];

    // ...
    }

    und verifizieren die Anzahl der Aufruf in tearDown:

  • (void)tearDown { [databaseMock verify]; [user release]; }

    Die Methode verify überprüft nach jedem Test, ob die mit expect definierte Stub-Methoden jeweils genau einmal aufgerufen wurde (was im Beispiel der Fall ist, da vor jedem Test in setUp die Instanz user gespeichert wird). [Revision 11065e8d]

    Zweiter Nachgang¶

    Wie würde die Lösung aussehen, wenn man kein Mock-Objekt verwenden will, sondern die Anbindung der Datenbank in UserDatabase bewusst mittesten möchte? SQLite erlaubt die Erstellung einer Datenbank im Hauptspeicher, so dass wir eine neue Methode setUpTestDatabase in UserTest.m wie folgt definieren

  • (void)setUpTestDatabase { userDatabase = [[UserDatabase alloc] initWithPath:@":memory:"]; [userDatabase.database executeUpdate: @"DROP TABLE users;"]; STAssertTrue([userDatabase.database executeUpdate: @"CREATE TABLE users (id INTEGER PRIMARY KEY NOT NULL, email VARCHAR(255), hashed_password VARCHAR(255));"], @""); [User setDatabase:userDatabase]; }

    und statt setUpMockDatabase in setUp aufrufen könnten:

  • (void)setUp { user = [User userWithEmail:@"thomas@dohmke.de" password:@"foobar"]; [self setUpTestDatabase]; STAssertEquals([user save], YES, @"save failed."); }

    Es ist zu beachten, dass die SQL-Befehle in setUpTestDatabase hier nur beispielhaft aufgeführt werden, im realen Leben würde man die Klasse UserDatabase um entsprechende Methoden erweitern, die diese kapseln und damit Dopplungen im Code vermeiden.

]]>
Wed, 04 Mar 2009 16:16:00 +0100 http://thomas.dohmke.de/items/view/199/unit-testing-in-objective-c-teil-3
Unit-Testing in Objetive-C, Teil 2 http://thomas.dohmke.de/items/view/166/unit-testing-in-objetive-c-teil-2

Der zweite Teil unserer Serie zu Unit-Testing in Objective-C beschäftigt sich mit den Assertions. Sie beschreiben die Behauptungen bzw. Erwartungen, die innerhalb einer Testmethode an das Testobjekt gestellt werden. OCUnit stellt dafür verschiedene Makros zur Verfügung:

STAssertEquals¶

In Teil 1 haben wir bereits das Makro "STAssertEquals" benutzt.

Definition:
STAssertEquals(a1, a2, description, ...);

Beispiel: STAssertEquals(4.0, exp2(2), @"2 times 2 shall be 4.");

Es vergleicht die Werte "a1" und "a2" miteinander und gibt eine Fehlermeldung inklusive des Wertes von "description" aus, wenn diese nicht gleich sind. Für "description" kann ein Formatstring (wie für "printf") definiert werden, entsprechend folgen die durch Komma getrennten Parameter.

STAssertEqualsWithAccuracy¶

Insbesondere bei Verwendung von Fließkommazahlen empfiehlt es sich, statt "STAssertEquals" das Makro "STAssertEqualsWithAccuracy" zu verwenden. Dieses erlaubt die Festlegung einer Abweichung, innerhalb der der erwartete Wert liegen darf.

Definition:
STAssertEqualsWithAccuracy(left, right, accuracy, description, ...);

Beispiel: STAssertEqualsWithAccuracy(1.0, cos(0), 0.001, @"cos(0) shall be 1, but we allow a tolerance of 0.001.");

Der Parameter "accuracy" legt die maximale Differenz zwischen beiden Werten fest, im Beispiel darf der Wert von "cos(0)" also um 0.001 nach oben und unten von 1.0 abweichen.

STAssertEqualObjects¶

Beim Vergleich zweier Objekte ist häufig nicht die Frage, ob die Instanzen selbst identisch sind, sondern ob ihre Werte bzw. die Werte ihrer Instanzvariablen gleich sind. Ein typischer Fall ist der Vergleich zweier NSString-Objekte. OCUnit bietet dafür das Marko "STAssertEqualObjects", welches auf die "isEqual"-Methode der jeweiligen Klasse zurückgreift:

Definition:
STAssertEqualObjects(a1, a2, description, ...);

Beispiel: NSString *string = [NSString stringWithUTF8String:"Just a test"]; STAssertEqualObjects(string, @"Just a test", @"Both strings shall be equal.");

Wer den Unterschied zu "STAssertEquals" sehen will, kann probehalber auch folgenden Aufruf probieren:

STAssertEquals(string, @"Just a test", @"Both strings shall be equal.");

Während die erste Zusicherung erfüllt wird, schlägt die zweite fehl und Xcode zeigt die Meldung "<20dd1900> should be equal to <60610300>: Both strings shall be equal." an. Wie erwartet, werden die Speicheradressen der Objekte und nicht deren Werte verglichen.

STAssertTrue und STAssertFalse¶

Diese beiden Makros überprüfen boolesche Ausdrücke bzw. Funktionen, die boolesche Werte zurückgeben.

Definition:
STAssertTrue(expression, description, ...); STAssertFalse(expression, description, ...);

Beispiel: STAssertTrue([@"Hesse" isGreaterThan:@"Goethe"], @"Hesse is greater than Goethe"); STAssertFalse([@"Hesse" isGreaterThan:@"Rilke"], @"Hesse is not greater than Rilke");

Man sollte "STAssertTrue" nicht für Vergleiche mit "==" oder "isEqual" verwenden, da hier "STAssertEquals" sowie "STAssertEqualObjects" besser geeignet sind und die verglichenen Werte in der Fehlermeldung aufführen (fügt man diese per entsprechenden Token in den Formatstring von "description" ein, kann man selbiges auch mit "STAssertTrue" erreichen, dies ist aber nicht Sinn der Sache).

STAssertNil und STAssertNotNil¶

"STAssertNil" vergleicht den übergebenen Wert mit "nil", "STAssertNotNil" sichert zu, dass er nicht "nil" ist. Der vorrangige Einsatzzweck sind Funktionen, die Zeiger auf Objekte zurückgeben.

Definition:
STAssertNil(a1, description, ...); STAssertNotNil(a1, description, ...);

Beispiel: NSMutableDictionary *dict = [[NSMutableDictionary alloc] init]; [dict setObject:@"TextMate" forKey:@"editor"]; STAssertNil([dict objectForKey:@"viewer"], @"Shall return nil as key is not set in dictionary."); STAssertNotNil([dict objectForKey:@"editor"], @"Shall return not nil as key is set in dictionary.");

Im Beispiel wird ein "NSMutableDictionary" mit einem Eintrag mit dem Key "editor" erstellt, entsprechend liefert es für diesen Key ein Objekt zurück. Für andere Keys wird hingegen "nil" zurückgegeben.

STAssertThrows und STAssertNoThrow¶

Diese Assertions erwarten, dass eine Exception ausgelöst bzw. nicht ausgelöst wird.

Definition:
STAssertThrows(expression, description, ...); STAssertNoThrow(expression, description, ...);

Beispiel: NSMutableArray *array = [[NSMutableArray alloc] init]; [array addObject:@"Xcode"]; STAssertNoThrow([array objectAtIndex:0], @"No exception as index exists."); STAssertThrows([array objectAtIndex:1], @"Exception thrown as index is beyond bounds.");

Zusätzlich gibt es noch die Varianten "STAssertThrowsSpecific" und "STAssertThrowsSpecificNamed" sowie "STAssertNoThrowSpecific" und "STAssertNoThrowSpecificNamed", mit denen sich Typ und Name der Exception spezifieren lassen. Beispiel:

STAssertThrowsSpecificNamed([array objectAtIndex:1], NSException, NSRangeException, @"NSRangeException thrown as index is beyond bounds.");

Außerdem können mit "STAssertTrueNoThrow" und "STAssertFalseNoThrow" boolesche Ausdrücke ausgewertet und gleichzeitig überprüft werden, dass keine Exception erfolgt. Zwar zeigt Xcode auch ohne diese Varianten an, wenn eine Exception ausgelöst wurde, sie ermöglichen jedoch die Darstellung der Fehlermeldung direkt in der betroffenen Zeile.

STFail¶

Last, but not least, lässt das Makro "STFail" jeden Testfall fehlschlagen. Die Motivation dafür ist typischerweise, dass man die betroffene Methode als fehlerhaft oder unfertig markieren und dadurch deutlich machen will, dass noch Handlungsbedarf besteht.

Definition:
STFail(description, ...);

Beispiel: STFail(@"Test not finished yet.");

Damit endet dieser Artikel. Im nächsten Teil der Serie werden wir uns mit Mock-Objekten beschäftigen.

]]>
Fri, 13 Feb 2009 16:17:00 +0100 http://thomas.dohmke.de/items/view/166/unit-testing-in-objetive-c-teil-2
Unit-Testing in Objetive-C, Teil 1 http://thomas.dohmke.de/items/view/128/unit-testing-in-objetive-c-teil-1

Die testgetriebene Entwicklung (Test-Driven Development, abgekürzt TDD) ist eine mittlerweile weit verbreitete Praktik des Extreme Programming bzw. der agilen Entwicklungsmethodik. Mit TDD wird die eigentliche Funktionalität erst dann programmiert, wenn mindestens ein Test fehlschlägt. Für fast alle Programmiersprachen existieren entsprechende Testing-Frameworks, die die Definition, Ausführung und Verifikation der Tests maßgeblich unterstützen. Für Objetive-C wurde ein solches Framework names OCUnit ursprünglich von der schweizer Firma Sen:te entwickelt und im Jahr 2005 von Apple offiziell in Xcode 2.1 integriert. Dennoch liest man (im deutschsprachigen Raum) recht wenig über TDD in Zusammenhang mit Objective-C. Der nachfolgende Artikel soll dahingehend Abhilfe schaffen und ist der Anfang einer kleinen Serie über Unit-Testing für die Entwicklung von Programmen für OS X auf Mac und iPhone.

Ausgangsbasis¶

Im ersten Teil wollen wir beschreiben, wie man ein einfaches Cocoa-Projekt um die Möglichkeit des Unit-Testings erweitert und Testfälle definiert. Als Entwicklungsumgebung wird Xcode 3 benötigt, konkret haben wir Xcode 3.1.2 verwendet, welches mit iPhone SDK 2.2 ausgeliefert wurde. Ferner steht das Projekt als Git-Repository zur Verfügung. Man kann es entweder lokal klonen,

git clone git://www.komprovisation.de/objcut1.git

oder auf der Projektseite anschauen und durchstöbern. Dies hat den Vorteil, dass das Projekt nicht nur als finales Ergebnis runtergeladen werden kann, sondern auch die einzelnen Schritte nachvollziehbar sind. Entsprechende Stellen sind im Artikel durch die jeweilige Revision vermerkt.

Erste Iteration¶

Das Ziel der ersten Iteration ist es, ein neues Projekt in Xcode zu erstellen und dieses durch ein sogenanntes Unit Test Bundle zu erweitern. Sofern nicht schon geschehen, starten wir Xcode und wählen den Menüpunkt "File > New Project" aus. Im folgenden Dialog wählen wir in der linken Spalte unterhalb von Mac OS X den Punkt "Application" aus und dann das Template "Cocoa Application". Weiter geht's mit "Choose", wir geben dem Projekt den Namen "ObjCUT1", wählen ein Verzeichnis aus und erstellen das Projekt mit Klick auf den Button "Save". Es erscheint das nachfolgende Fenster mit dem Projekt. [Revision bcb32406]

Durch Klick auf den Button "Build & Go" oder alternativ Cmd-R kompilieren wir das Projekt und führen es anschließend aus. Es öffnet sich nach kurzer Zeit unser Programm mit einem leeren Fenster.

Zurück zum Projektfenster. Dort finden wir in der linken Spalte "Groups & Files" die Gruppe "Targets". Einziges Mitglied ist bisher das Target "ObjCUT1", welches für die gerade probierte Kompilierung unseres Programms zuständig ist. Durch einen Rechtsklick auf "Target" und die Auswahl des Menüpunkts "Add > New Target" fügen wir nun ein neues Target hinzu. Es öffnet sich ein ähnlicher Dialog wie vorher bei der Erstellung des Projektes, in der linken Spalte wählen wir dieses Mal "Cocoa" aus, dann im rechten Bereich das "Unit Test Bundle" und weiter geht's mit "Next". Im nächsten Dialog geben wir als Name des Target "UnitTests" ein und bestätigen mit "Finish". Die Wahl des Namens ist grundsätzlich egal, einzige Bedingung ist, dass nicht schon ein Target mit gleichem Namen existiert. Im Projektfenster sehen wir jetzt zwei Targets, zusätzlich öffnet sich automatisch das Info-Fenster für das neu erstellte Target (falls nicht, kann man das Fenster entweder per Rechtsklick auf das Target und den Menüpunkt "Get Info" oder durch Linksklick und Cmd-I öffnen).

Im Reiter "General" des Info-Fensters gibt es zwei Listen: "Direct Dependencies" und "Linked Libaries". Wir interessieren uns hier nur für die erste. Direct Dependencies sind solche Targets, von denen unser Unit-Testing-Target abhängig ist. Xcode stellt vor dem Kompilieren sicher, ob diese Abhängigkeiten auf dem neuesten Stand sind oder zunächst kompiliert werden müssen. Wir fügen der Liste einen neuen Eintrag hinzu, indem wir auf den Button mit dem Pluszeichen klicken, das Target "ObjCUT1" auswählen und mit "Add Target" bestätigen.

Als nächstes wechseln wir zum Reiter "Build aus", wo zwei Einstellungen zu setzen sind. Am einfachsten ist dies durch Eingabe des Namens der Einstellung in das Suchfeld. Die erste Einstellung heißt "Bundle Loader" und wir setzen sie auf

$(BUILT_PRODUCTS_DIR)/ObjCUT1.app/Contents/MacOS/ObjCUT1

"BUILT_PRODUCTS_DIR" ist dabei eine definierte Konstante innerhalb von Xcode, welche auf das build-Verzeichnis in unserem Projekt zeigt, z.B. ~/Projects/ObjCUT1/build/Debug. Der restliche Pfad referenziert die ausführbare Datei innerhalb des Application-Bundles.

Die zweite Einstellung heißt "Test Host" und wir setzen sie auf

$(BUNDLE_LOADER)

"BUNDLE_LOADER" ist wiederum eine definierte Konstante, die der obigen Einstellung entspricht. Mit anderen Worten haben beide Einstellungen damit denselben Wert, nur haben wir uns bei der zweiten Einstellung ein wenig Tippaufwand gespart. Abschließend können wir das Info-Fenster schließen.

Im Projektfenster wählen wir nun ganz links in der Symbolleiste unterhalb des Punktes "Active Target" das neue Target "UnitTests" aus und kompilieren mit "Build & Go". Wenn alles geklappt hat, kompiliert das Target ohne Fehlermeldung und es öffnet sich das Programm mit seinem leerem Fenster. [Revision 88f16a29]

Zweite Iteration¶

In der zweiten Iteration wollen wir nun eine einfache Klasse mit Namen "User" testgetrieben entwickeln. Die Klasse soll innerhalb einer Model-View-Controller Architektur ein Model für die Benutzerverwaltung darstellen. Wir werden nicht die vollständige Klasse entwickeln, sondern uns auf eine einzige Beispielmethode beschränken.

Im Sinne von TDD starten wir mit einem Testfall. Im Projektfenster klicken wir mit der rechten Maustaste auf die Gruppe "Classes" und wählen "Add > New File" aus. In der linken Spalte selektieren wir "Cocoa", im rechten Feld die Vorlage "Objective-C test case class". Weiter mit "Next". Der Name der Testfallklasse soll sich aus dem Namen der zu entwickelnden Klasse und dem Suffix "Test" zusammensetzen, d.h. wir geben "UserTest.m" ein. Außerdem deselektieren wir in der Liste "Targets" den Eintrag "ObjCUT1" und selektieren stattdessen das Target "UnitTests". Nach Klick auf "Finish" werden die Dateien erstellt. Mit "Build > Build" oder Cmd-B versichern wir uns, dass keine Fehler auftreten.

Um im TDD-Zyklus voran zu kommen, ist der nächste Schritt, einen Test fehlschlagen zu lassen. Dazu öffnen wir UserTest.m und erstellen eine Testmethode:

import "UserTest.h"

@implementation UserTest

-(void)testValidLogin { STAssertEquals([user loginWithEmail:@"thomas@dohmke.de" password:@"foobar"], YES, @"should return YES"); }

@end

OCUnit identifiziert Testmethoden wie auch viele andere Testing Frameworks anhand der Präfixes "test", unsere Methode heißt endsprechend "testValidLogin". Innerhalb der Methode wird das Makro "STAssertEquals" aufgerufen, welches drei Parameter akzeptiert: Der dritte Parameter wird als Fehlermeldung ausgegeben, wenn die ersten zwei Parameter nicht übereinstimmen. In diesem Fall wird außerdem die Testmethode abgebrochen, d.h. weitere Anweisungen innerhalb derselben Methode werden nicht ausgeführt. Im Beispiel wird die Nachricht "loginWithEmail:password:" an ein Objekt "user" gesendet und der Rückgabewert soll dem Wert "YES" entsprechen.

Beim Kompilieren schlägt diese Testmethode offensichtlich fehl. Xcode zeigt uns dies direkt unter der betroffenen Zeile an.

Der zweite Schritt des TDD-Zyklusses ist es, diesen Test erfolgreich zu bestehen. Wir arbeiten uns dazu Schritt für Schritt durch die Fehlermeldungen. Der Compiler kennt das Objekt "user" nicht, Abhilfe schafft die entsprechende Deklaration und anschließende Initialisierung:

import "UserTest.h"

import "User.h"

@implementation UserTest

-(void)testValidLogin { User *user; user = [[User alloc] init]; STAssertEquals([user loginWithEmail:@"thomas@dohmke.de" password:@"foobar"], YES, @"should return YES"); [user release] }

@end

Nun fügen wir die Klasse "User" zu unserem Projekt hinzu. Rechtsklick auf "Classes", "Add > New File", links sollte noch "Cocoa" ausgewählt sein, rechts wählen wir dieses Mal "Objective-C class", gehen weiter mit "Next", geben als Name "User.m" ein und selektieren beide Targets. Bestätigen mit "Finish" und kompilieren mit Cmd-B. [Revision cf86f69d]

Die nächste Fehlermeldung bemängelt, dass die Klasse "User" die Nachricht "loginWithEmail:password:" nicht kennt. Wir deklarieren diese in der Datei User.h

import <Cocoa/Cocoa.h>

@interface User : NSObject { }

-(BOOL)loginWithEmail:(NSString *)email password:(NSString *)password;

@end

und definieren sie anschließend in User.m:

import "User.h"

@implementation User

-(BOOL)loginWithEmail:(NSString *)email password:(NSString *)password { return YES; }

@end

Die Implementierung besteht getreu dem Motto "Fake it till you make it" lediglich aus dem Zurückgeben von "YES". Der Build-Prozess läuft nun ohne Fehler durch und wir können mit Cmd-R die Anwendung starten. [Revision 0afffba8]

Dritte Iteration¶

Die dritte Iteration startet mit einer weiteren Testmethode, die im Gegensatz zu oben den Negativfall, d.h. den Login mit falschen Benutzerdaten, überprüft:

-(void)testInvalidLogin { User *user; user = [[User alloc] init]; STAssertEquals([user loginWithEmail:@"thomas@dohmke.de" password:@"wrong"], NO, @"should return NO"); [user release]; }

Zwei Punkte gefallen uns auf:

Ein Teil der Methode entspricht einer exakten Kopie der ersten Methode. Die "release"-Methode für "user" wird nie aufgerufen, da der Aufruf von "STAssertEquals" fehlschlägt.

Eine Lösung bietet die so genannte Test Fixture, welche den Kontext beschreibt, den die Testmethoden für ihre Ausführung voraussetzen. Sie wird in OCUnit wie in vielen anderen Testing Frameworks über die Methoden "setUp" und "tearDown" gebildet. setUp wird vor jeder Testmethode aufgerufen, tearDown garantiert danach und zwar unabhängig davon, ob der Test fehlgeschlagen ist oder nicht. Unser Beispiel sieht damit wie folgt aus: [Revision 5434f7d4]

import "UserTest.h"

@implementation UserTest

-(void)setUp { user = [[User alloc] init]; }

-(void)tearDown { [user release]; }

-(void)testValidLogin { STAssertEquals([user loginWithEmail:@"thomas@dohmke.de" password:@"foobar"], YES, @"should return YES"); }

-(void)testInvalidLogin { STAssertEquals([user loginWithEmail:@"thomas@dohmke.de" password:@"wrong"], NO, @"should return NO"); }

@end

Die Variable "user" wird entsprechend in der Header-Datei definiert:

import <SenTestingKit/SenTestingKit.h>

import "User.h"

@interface UserTest : SenTestCase { User *user; }

@end

Zum Schluss wollen wir noch die Klasse "User" so verändern, dass beide Tests bestanden werden. Eine reale Lösung würde dazu auf eine Datenbank oder Konfigurationsdatei zurückgreifen, wir bleiben hier bei der einfachstmöglichen Lösung und vertagen den Rest auf später: [Revision a2e14c79]

-(BOOL)loginWithEmail:(NSString *)email password:(NSString *)password { return [email isEqualToString:@"thomas@dohmke.de"] && [password isEqualToString:@"foobar"]; }

Alle Tests werden bestanden und so endet dieser Artikel. Kommentare, Fragen und Kritik sind wie immer herzlich willkommen. Fortsetzung folgt... :)

]]>
Fri, 23 Jan 2009 12:33:00 +0100 http://thomas.dohmke.de/items/view/128/unit-testing-in-objetive-c-teil-1