thomas.dohmke.de - tagged with xcode 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
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
Drei Links zum Sonntag http://thomas.dohmke.de/items/view/157/drei-links-zum-sonntag

Diese Woche war es sehr ruhig hier, aber auf die Links zum Sonntag wollen wir nicht verzichten. Dieses Mal ein Link zum Exception-Handling in Mac- und iPhone-Programmen und zwei zum Thema Xcode:

Open Source Crash Reporter for iPhone Landon Fuller veröffentlichte im Lauf der Woche ein Framework für das Exception-Handling innerhalb von Mac- und iPhone-Programmen. Crash-Reports werden als Protocol Buffers gespeichert und können beim nächsten Programmstart beispielsweise per E-Mail versendet werden. 14 Essential Xcode Tips, Tricks and Resources for iPhone Devs Dan Grigsby fasst bei MobileOrchard die 14 wichtigsten Tips zu Xcode zusammen. Von der Fensterkonfiguration über Shortcuts bis hin zu Skripten ist alles dabei. Complete Xcode Keyboard Shortcut List Colin Wheeler stellt in einem Artikel, der schon aus dem Februar 2008 stammt, sämtliche Shortcuts in Xcode in Form einer Übersicht als PNG- oder PDF-Datei zur Verfügung.

Mein persönlicher Lieblingsshortcut ist übrigens ⌘⇧D, die meiner Meinung nach schnellste Möglichkeit, um Dateien in Xcode zu öffnen.

]]>
Sun, 01 Feb 2009 20:05:00 +0100 http://thomas.dohmke.de/items/view/157/drei-links-zum-sonntag
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
Kurztip: Xcode deinstallieren http://thomas.dohmke.de/items/view/80/kurztip-xcode-deinstallieren

Wer Xcode und mitgelieferte Entwicklerwerkzeuge unterhalb des Verzeichnisses /Developer deinstallieren will, dem sei mit folgendem Befehl im Terminal geholfen:

sudo /Developer/Library/uninstall-devtools --mode=all

Anschließend können bei Bedarf auch die benutzerspezifischen Einstellungen gelöscht werden:

rm ~/Library/Preferences/com.apple.Xcode.plist rm ~/Library/Preferences/com.apple.InterfaceBuilder3.plist rm ~/Library/Preferences/com.apple.Instruments.plist

Im Fall einer anschließenden Neuinstallation ist dann jedoch der Firmenname erneut zu setzen.

]]>
Tue, 13 Jan 2009 14:14:00 +0100 http://thomas.dohmke.de/items/view/80/kurztip-xcode-deinstallieren
XCode-Projekte in Git anlegen http://thomas.dohmke.de/items/view/58/xcode-projekte-in-git-anlegen

Um ein XCode-Projekt in Git anzulegen, habe ich ein kleines Shell-Skript geschrieben (Download), das folgende Schritte ausführt:

Ein leeres Git-Repository erzeugen. Die Datei .gitignore mit folgendem Inhalt erstellen:build .pbxuser *.mode1v3 .DS_StoreDamit werden das build-Verzeichnis, benutzerspezifische Dateien sowie die von Mac OS X angelegten .DS_Store-Müllhalden von der Versionierung ausgeschlossen. Die Datei .gitattributes mit folgendem Inhalt erstellen:.pbxproj -crlf -diff -mergeDer Parameter "-crlf" bewirkt, dass für Dateien mit der Endung .pbxproj keine Transformation der Zeilenumbrüche vorgenommen wird, "-diff" und "-merge" schließt den Vergleich (diff) und die Zusammenführung (merge) mit vorherigen Versionen aus. Den derzeitigen Stand in das Repository einchecken.

Der Aufruf im Terminal sieht dann wie folgt aus (nachfolgend als Beispiel für ein Cocoa-Projekt, das unter ~/Projects/PeachApp abgelegt wurde): $ cd ~/Projects/PeachApp $ xcode-git-init.sh Initialized empty Git repository in ~/Projects/PeachApp/.git/ Creating .gitignore. Creating .gitattributes. Commiting initial revision. add '.gitattributes' add '.gitignore' add 'English.lproj/InfoPlist.strings' add 'English.lproj/MainMenu.xib' add 'Info.plist' add 'PeachApp.xcodeproj/TemplateIcon.icns' add 'PeachApp.xcodeproj/project.pbxproj' add 'PeachApp_Prefix.pch' add 'main.m' Created initial commit 902b78f: Initial revision. 9 files changed, 3088 insertions(+), 0 deletions(-) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 English.lproj/InfoPlist.strings create mode 100644 English.lproj/MainMenu.xib create mode 100644 Info.plist create mode 100644 PeachApp.xcodeproj/TemplateIcon.icns create mode 100644 PeachApp.xcodeproj/project.pbxproj create mode 100644 PeachApp_Prefix.pch create mode 100644 main.m Finished. Have fun.

So bekommt man einfach und schnell ein Repository für ein XCode-Projekt, egal, ob es sich dabei um das nächste große Ding handelt oder nur um einen Prototypen. Denn wie schrieben Andy Hunt und Dave Thomas schon 1999:

Always Use Source Code Control. Always. Even if you are a single-person team on a one-week project. Even if it's a "throw-away" prototype. Even if the stuff you're working on isn't source code. Make sure that everything is under source code.

Aus The Pragmatic Programmer, Kapitel 17, Seite 86ff.

]]>
Thu, 08 Jan 2009 21:23:00 +0100 http://thomas.dohmke.de/items/view/58/xcode-projekte-in-git-anlegen